Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0dcfc4d
Initial addition of Subsonic Connection
davidvedvick Mar 26, 2025
541c77e
Implemented Subsonic promiseServerVersion
davidvedvick Apr 3, 2025
84711bf
Added hard-coded playlist item
davidvedvick Apr 6, 2025
ea9b4fb
Added item access
davidvedvick Apr 7, 2025
8995d78
Added ServerTypeSelectionViewModel
davidvedvick Apr 7, 2025
5a55bb8
Added initial LibrarySettingsView
davidvedvick Apr 8, 2025
b626aad
Added Subsonic connection storage
davidvedvick Apr 9, 2025
4d51a72
Updated LibrarySettingsViewModel to work with Subsonic connections
davidvedvick Apr 10, 2025
4be8f50
Added Subsonic connection settings to view
davidvedvick Apr 10, 2025
aa828be
Validated Subsonic connection settings
davidvedvick Apr 11, 2025
b5a57af
Provided Live Subsonic Connections
davidvedvick Apr 11, 2025
1861547
Fixed password hashing
davidvedvick Apr 11, 2025
69d1006
Created shared Gson instance
davidvedvick Apr 11, 2025
0a2b8a3
Implemented subsonic play stat updates
davidvedvick Apr 11, 2025
7bb03cc
Handle non-200 responses for subsonic updates
davidvedvick Apr 11, 2025
e736d06
Implemented streaming music
davidvedvick Apr 11, 2025
0afad26
Moved server version based file property updates to connection provider
davidvedvick Apr 12, 2025
1883bdd
Implemented getting file properties
davidvedvick Apr 12, 2025
0747af2
Implemented getting file lyrics
davidvedvick Apr 13, 2025
e7c8918
Implemented getting file lists
davidvedvick Apr 13, 2025
0385469
Improved file properties access cancellation
davidvedvick Apr 13, 2025
b34636c
Implemented revision access
davidvedvick Apr 14, 2025
0f95643
Setup test path correctly
davidvedvick Apr 14, 2025
328d36e
Cached server version
davidvedvick Apr 14, 2025
20d208c
Added support for browsing to playlists
davidvedvick Apr 15, 2025
4ceed21
Returned IItem from item providers
davidvedvick Apr 15, 2025
c566741
Used KeyedIdentifier as item browsing input
davidvedvick Apr 15, 2025
58edd69
Implemented getting files via PlaylistId
davidvedvick Apr 17, 2025
b1d31b4
Implemented getting file "string lists" for playlists
davidvedvick Apr 17, 2025
a76f7b9
Added support for getting files associated with PlaylistIds
davidvedvick Apr 17, 2025
ab69966
Added cache policies to library files provider
davidvedvick Apr 18, 2025
879db6d
Handle errors in subsonic response
davidvedvick Apr 18, 2025
7b30c65
Moved library revision caching
davidvedvick Apr 18, 2025
42899c2
Implemented getting subsonic images
davidvedvick Apr 18, 2025
60cd22f
Returned null when ServerVersion throws error
davidvedvick Apr 18, 2025
a54dc58
Implemented isReadOnly check using getUser API
davidvedvick Apr 19, 2025
5c47976
Implemented creating playlists
davidvedvick Apr 19, 2025
a2f176c
Implemented getting playlist paths
davidvedvick Apr 19, 2025
1f39482
Implemented song search
davidvedvick Apr 19, 2025
f01668a
Returned empty file list for special items
davidvedvick Apr 19, 2025
939498b
Moved json format parameter to live connection
davidvedvick Apr 19, 2025
32670d3
Treat admin role as not read-only
davidvedvick Apr 19, 2025
57865ab
Fixed updating playlists
davidvedvick Apr 19, 2025
358109d
Returned file properties as editable/read-only
davidvedvick Apr 21, 2025
8bd6ddc
Treat properties that are `EditableFileProperty` as editable
davidvedvick Apr 21, 2025
3fa6226
Added setting Subsonic ratings
davidvedvick Apr 21, 2025
a91d263
Fixed getting root items from Media Center servers
davidvedvick Apr 22, 2025
6bb37db
Use saved server type for connection settings
davidvedvick Apr 22, 2025
eb43a09
Increased the settings menu height
davidvedvick Apr 23, 2025
e55f278
Fixed saving new settings connection type
davidvedvick May 8, 2025
6db7bbd
Updated README with Subsonic support
davidvedvick May 13, 2025
f9126bf
Fixed order of error notification
davidvedvick May 13, 2025
5f240c6
Supported syncing with subsonic servers
davidvedvick May 14, 2025
7994d7f
Supported getting replayGain
davidvedvick May 14, 2025
7799adc
Optimized `FuturePromise`
davidvedvick May 15, 2025
7173a8c
Synchronized now playing state on outside promise
davidvedvick May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

![project blue](./design/project-blue-logo-circular.png)

### An alternative, open source streaming music client for [JRiver Media Center](http://jriver.com/).
### An alternative, open source streaming music client

project blue is a streaming audio client for JRiver Media Center (http://jriver.com/). Stream your favorite music and audio from your JRiver Media Center wherever you are!
project blue is a streaming audio client for various servers. Stream your favorite music and audio from your server wherever you are!

Its features include:

* Reliable streaming from your home server running JRiver Media Center via an intuitive layout.
* Reliable streaming from your home server via an intuitive layout.
* [JRiver Media Center](http://jriver.com/)
* Subsonic (alpha - tested with [Navidrome](https://www.navidrome.org/) only)
* Caching of audio files during playback.
* Synchronize audio from JRMC server to device.
* Synchronize audio from server to device.
* Play local files when present and metadata match.
* Updates server with playback statistics.
* Edit and update playlists through Now Playing.
Expand All @@ -21,8 +23,6 @@ Its features include:

Download on the [Google Play Store](https://play.google.com/store/apps/details?id=com.lasthopesoftware.bluewater)

*Requires [JRiver Media Center](http://jriver.com/) running on your home server*

# Development

![](https://github.com/actions/namehillsoftware/projectBlue/workflows/.github/workflows/build.yml/badge.svg)
Expand Down Expand Up @@ -106,6 +106,7 @@ file for details.
## Acknowledgments

- [JRiver Media Center](https://jriver.com/)
- [Navidrome](https://www.navidrome.org/)
- [ExoPlayer](https://github.com/google/ExoPlayer)
- [Lightweight Stream API](https://github.com/aNNiMON/Lightweight-Stream-API)
- [RxJava](https://github.com/ReactiveX/RxJava)
Expand Down
2 changes: 1 addition & 1 deletion projectBlueWater/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ dependencies {
implementation "androidx.compose.runtime:runtime-rxjava3:$compose_version"
implementation 'androidx.activity:activity-compose:1.10.1'
implementation 'dev.olshevski.navigation:reimagined:1.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0"
implementation 'com.google.code.gson:gson:2.12.1'
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
testImplementation 'commons-codec:commons-codec:1.18.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.lasthopesoftware.resources.bitmaps.DefaultAwareCachingBitmapProducer
import com.lasthopesoftware.resources.bitmaps.QueuedBitmapProducer
import com.lasthopesoftware.resources.network.ActiveNetworkFinder
import com.lasthopesoftware.resources.strings.Base64Encoder
import com.lasthopesoftware.resources.strings.JsonEncoderDecoder
import com.lasthopesoftware.resources.strings.StringResources

object ApplicationDependenciesContainer {
Expand Down Expand Up @@ -157,7 +158,7 @@ object ApplicationDependenciesContainer {
get() = libraryRepository

override val librarySettingsProvider by lazy {
val access = LibrarySettingsAccess(libraryStorage)
val access = LibrarySettingsAccess(libraryStorage, JsonEncoderDecoder)
CachedLibrarySettingsAccess(access, access)
}

Expand Down Expand Up @@ -202,7 +203,9 @@ object ApplicationDependenciesContainer {
serverLookup,
connectionSettingsLookup,
okHttpClients,
okHttpClients
okHttpClients,
JsonEncoderDecoder,
stringResources,
),
audioCacheStreamSupplier,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.lasthopesoftware.bluewater.client.access

import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile
import com.lasthopesoftware.bluewater.client.browsing.items.Item
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableFilePropertyDefinition
import com.lasthopesoftware.bluewater.client.browsing.items.IItem
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
import com.lasthopesoftware.bluewater.client.browsing.items.KeyedIdentifier
import com.lasthopesoftware.bluewater.client.browsing.items.playlists.PlaylistId
import com.lasthopesoftware.bluewater.client.servers.version.SemanticVersion
import com.namehillsoftware.handoff.promises.Promise
Expand All @@ -17,20 +19,23 @@ interface RemoteLibraryAccess {
isFormatted: Boolean
): Promise<Unit>

fun promiseItems(itemId: ItemId?): Promise<List<Item>>
fun promiseItems(itemId: KeyedIdentifier?): Promise<List<IItem>>
fun promiseAudioPlaylistPaths(): Promise<List<String>>
fun promiseStoredPlaylist(playlistPath: String, playlist: List<ServiceFile>): Promise<*>
fun promiseIsReadOnly(): Promise<Boolean>
fun promiseServerVersion(): Promise<SemanticVersion?>
fun promiseRevision(): Promise<Int?>
fun promiseRevision(): Promise<Long?>
fun promiseFile(serviceFile: ServiceFile): Promise<InputStream>
fun promisePlaystatsUpdate(serviceFile: ServiceFile): Promise<*>
fun promiseFiles(): Promise<List<ServiceFile>>
fun promiseFiles(query: String): Promise<List<ServiceFile>>
fun promiseFiles(itemId: ItemId): Promise<List<ServiceFile>>
fun promiseFiles(playlistId: PlaylistId): Promise<List<ServiceFile>>
fun promiseFileStringList(itemId: ItemId? = null): Promise<String>
fun promiseFileStringList(playlistId: PlaylistId): Promise<String>
fun promiseShuffledFileStringList(itemId: ItemId? = null): Promise<String>
fun promiseShuffledFileStringList(playlistId: PlaylistId): Promise<String>
fun promiseImageBytes(serviceFile: ServiceFile): Promise<ByteArray>
fun promiseImageBytes(itemId: ItemId): Promise<ByteArray>
fun promiseEditableFilePropertyDefinitions(): Promise<Set<EditableFilePropertyDefinition>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModelStoreOwner
import com.lasthopesoftware.bluewater.client.browsing.files.details.FileDetailsViewModel
import com.lasthopesoftware.bluewater.client.browsing.files.list.FileListViewModel
import com.lasthopesoftware.bluewater.client.browsing.files.list.SearchFilesViewModel
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableFilePropertyDefinitionProvider
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableLibraryFilePropertiesProvider
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListViewModel
import com.lasthopesoftware.bluewater.client.settings.LibrarySettingsViewModel
Expand All @@ -30,7 +31,7 @@ class ScopedViewModelRegistry(

override val fileListViewModel by viewModelStoreOwner.buildViewModelLazily {
FileListViewModel(
itemFileProvider,
libraryFilesProvider,
storedItemAccess,
)
}
Expand All @@ -49,17 +50,22 @@ class ScopedViewModelRegistry(

override val librarySettingsViewModel by viewModelStoreOwner.buildViewModelLazily {
LibrarySettingsViewModel(
libraryNameLookup = libraryNameLookup,
librarySettingsProvider = librarySettingsProvider,
librarySettingsStorage = librarySettingsStorage,
libraryRemoval = libraryRemoval,
applicationPermissions = permissionsDependencies.applicationPermissions,
stringResources = stringResources,
)
}

override val fileDetailsViewModel by viewModelStoreOwner.buildViewModelLazily {
FileDetailsViewModel(
connectionPermissions = connectionAuthenticationChecker,
filePropertiesProvider = EditableLibraryFilePropertiesProvider(freshLibraryFileProperties),
filePropertiesProvider = EditableLibraryFilePropertiesProvider(
freshLibraryFileProperties,
EditableFilePropertyDefinitionProvider(libraryConnectionProvider),
),
updateFileProperties = filePropertiesStorage,
defaultImageProvider = defaultImageProvider,
imageProvider = imageBytesProvider,
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.lasthopesoftware.bluewater.client.browsing.files.access

import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
import com.lasthopesoftware.bluewater.client.browsing.items.playlists.PlaylistId
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
import com.lasthopesoftware.policies.ExecutionPolicies
import com.namehillsoftware.handoff.promises.Promise

class DelegatingLibraryFileProvider(inner: ProvideLibraryFiles, policies: ExecutionPolicies) : ProvideLibraryFiles {
private val promiseLibraryFiles by lazy { policies.applyPolicy<LibraryId, List<ServiceFile>>(inner::promiseFiles) }
private val promiseItemFiles by lazy { policies.applyPolicy<LibraryId, ItemId, List<ServiceFile>>(inner::promiseFiles) }
private val promisePlaylistFiles by lazy { policies.applyPolicy<LibraryId, PlaylistId, List<ServiceFile>>(inner::promiseFiles) }
private val promiseAudioFilesPolicy by lazy { policies.applyPolicy<LibraryId, String, List<ServiceFile>>(inner::promiseAudioFiles) }

override fun promiseFiles(libraryId: LibraryId): Promise<List<ServiceFile>> = promiseLibraryFiles(libraryId)

override fun promiseFiles(libraryId: LibraryId, itemId: ItemId): Promise<List<ServiceFile>> =
promiseItemFiles(libraryId, itemId)

override fun promiseFiles(libraryId: LibraryId, playlistId: PlaylistId): Promise<List<ServiceFile>> =
promisePlaylistFiles(libraryId, playlistId)

override fun promiseAudioFiles(libraryId: LibraryId, query: String): Promise<List<ServiceFile>> =
promiseAudioFilesPolicy(libraryId, query)
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ object FileStringListUtilities {
fun promiseSerializedFileStringList(serviceFiles: Collection<ServiceFile>): Promise<String> =
ThreadPools.compute.preparePromise { serializeFileStringList(serviceFiles, it) }

fun promiseShuffledSerializedFileStringList(serviceFiles: Collection<ServiceFile>): Promise<String> =
ThreadPools.compute.preparePromise { serializeFileStringList(serviceFiles.shuffled(), it) }

private fun serializeFileStringList(serviceFiles: Collection<ServiceFile>, cancellationSignal: CancellationSignal): String {
if (cancellationSignal.isCancelled) throw serializingCancelledException()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.lasthopesoftware.bluewater.client.browsing.files.access.stringlist

import com.lasthopesoftware.bluewater.client.browsing.files.access.parameters.FileListParameters
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
import com.lasthopesoftware.bluewater.client.browsing.items.playlists.PlaylistId
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
import com.lasthopesoftware.bluewater.client.connection.libraries.ProvideLibraryConnections
import com.lasthopesoftware.bluewater.client.connection.live.eventuallyFromDataAccess
Expand All @@ -11,10 +11,23 @@ import com.namehillsoftware.handoff.promises.Promise
class ItemStringListProvider(
private val libraryConnections: ProvideLibraryConnections
) : ProvideFileStringListForItem {
override fun promiseFileStringList(libraryId: LibraryId, itemId: ItemId?, options: FileListParameters.Options): Promise<String> {
// Put any crazy workarounds to get a fresh file list in here
return libraryConnections
override fun promiseFileStringList(libraryId: LibraryId, itemId: ItemId?): Promise<String> =
libraryConnections
.promiseLibraryConnection(libraryId)
.eventuallyFromDataAccess { a -> a?.promiseFileStringList(itemId).keepPromise("") }
}

override fun promiseFileStringList(libraryId: LibraryId, playlistId: PlaylistId): Promise<String> =
libraryConnections
.promiseLibraryConnection(libraryId)
.eventuallyFromDataAccess { a -> a?.promiseFileStringList(playlistId).keepPromise("") }

override fun promiseShuffledFileStringList(libraryId: LibraryId, itemId: ItemId?): Promise<String> =
libraryConnections
.promiseLibraryConnection(libraryId)
.eventuallyFromDataAccess { a -> a?.promiseShuffledFileStringList(itemId).keepPromise("") }

override fun promiseShuffledFileStringList(libraryId: LibraryId, playlistId: PlaylistId): Promise<String> =
libraryConnections
.promiseLibraryConnection(libraryId)
.eventuallyFromDataAccess { a -> a?.promiseShuffledFileStringList(playlistId).keepPromise("") }
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.lasthopesoftware.bluewater.client.browsing.files.access.stringlist

import com.lasthopesoftware.bluewater.client.browsing.files.access.parameters.FileListParameters
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
import com.lasthopesoftware.bluewater.client.browsing.items.playlists.PlaylistId
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
import com.namehillsoftware.handoff.promises.Promise

interface ProvideFileStringListForItem {
fun promiseFileStringList(libraryId: LibraryId, itemId: ItemId? = null, options: FileListParameters.Options = FileListParameters.Options.None): Promise<String>
fun promiseFileStringList(libraryId: LibraryId, itemId: ItemId? = null): Promise<String>
fun promiseShuffledFileStringList(libraryId: LibraryId, itemId: ItemId? = null): Promise<String>
fun promiseFileStringList(libraryId: LibraryId, playlistId: PlaylistId): Promise<String>
fun promiseShuffledFileStringList(libraryId: LibraryId, playlistId: PlaylistId): Promise<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.lasthopesoftware.bluewater.ActivityApplicationNavigation
import com.lasthopesoftware.bluewater.ApplicationDependenciesContainer.applicationDependencies
import com.lasthopesoftware.bluewater.android.intents.safelyGetParcelableExtra
import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableFilePropertyDefinitionProvider
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableLibraryFilePropertiesProvider
import com.lasthopesoftware.bluewater.client.browsing.files.properties.LibraryFilePropertiesDependentsRegistry
import com.lasthopesoftware.bluewater.client.browsing.items.list.ConnectionLostView
Expand Down Expand Up @@ -47,7 +48,10 @@ import java.io.IOException
}

private val filePropertiesProvider by lazy {
EditableLibraryFilePropertiesProvider(libraryConnectedDependencies.freshLibraryFileProperties)
EditableLibraryFilePropertiesProvider(
libraryConnectedDependencies.freshLibraryFileProperties,
EditableFilePropertyDefinitionProvider(localApplicationDependencies.libraryConnectionProvider),
)
}

private val libraryFilePropertiesDependents by lazy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.lasthopesoftware.bluewater.NavigateApplication
import com.lasthopesoftware.bluewater.R
import com.lasthopesoftware.bluewater.client.browsing.files.properties.FileProperty
import com.lasthopesoftware.bluewater.client.browsing.files.properties.FilePropertyType
import com.lasthopesoftware.bluewater.client.browsing.files.properties.KnownFileProperties
import com.lasthopesoftware.bluewater.client.browsing.files.properties.NormalizedFileProperties
import com.lasthopesoftware.bluewater.client.browsing.files.properties.ReadOnlyFileProperty
import com.lasthopesoftware.bluewater.shared.NullBox
import com.lasthopesoftware.bluewater.shared.android.colors.MediaStylePalette
import com.lasthopesoftware.bluewater.shared.android.colors.MediaStylePaletteProvider
Expand Down Expand Up @@ -261,7 +261,7 @@ fun FilePropertyRow(
val propertyValue by property.committedValue.subscribeAsState()

when (property.property) {
KnownFileProperties.Rating -> {
NormalizedFileProperties.Rating -> {
Box(
modifier = Modifier
.weight(2f)
Expand Down Expand Up @@ -360,7 +360,7 @@ private fun FileDetailsEditor(
contentAlignment = Alignment.Center
) {
when {
fileProperty.property == KnownFileProperties.Rating -> {
fileProperty.property == NormalizedFileProperties.Rating -> {
val ratingValue by remember { derivedStateOf { propertyValue.toInt() } }
RatingBar(
rating = ratingValue,
Expand Down Expand Up @@ -425,7 +425,7 @@ private fun FileDetailsEditor(
.navigable(
onClick = {
viewModel.activeLibraryId?.also {
navigateApplication.search(it, FileProperty(property, propertyValue))
navigateApplication.search(it, ReadOnlyFileProperty(property, propertyValue))
}
},
enabled = !isEditing
Expand Down
Loading