27 Commits
3.5.8 ... 3.6.0

Author SHA1 Message Date
antonio
112e468c7d gradle: bump up code version 2024-01-03 12:47:08 +01:00
antonio
0ccc601f1b gradle: add db migrations 2024-01-03 12:46:45 +01:00
CappielloAntonio
ae82bcd7bf Update github_release.yml 2024-01-03 12:40:21 +01:00
antonio
68512b7e12 feat: Implemented search functionality for Android Auto, "Made for You" section, starred songs, albums, artists, podcasts, and radio 2024-01-03 12:23:46 +01:00
antonio
d6cc4fc028 feat: test: Implemented initial functional version with Android Auto support 2024-01-03 00:45:22 +01:00
antonio
e8c7c065e2 gradle: dependencies update 2023-12-31 15:35:15 +01:00
CappielloAntonio
427394a105 Merge pull request #121 from DelightLane/localization
feat: add korean localization
2023-12-21 09:55:09 +01:00
CappielloAntonio
75fc2cbafa Merge pull request #118 from rkowalsk/french-localization-updates
fix: update French localization and fix previous errors
2023-12-21 09:53:54 +01:00
antonio
b9a3393b39 style: code cleanup 2023-12-21 09:52:10 +01:00
antonio
ed60677608 feat: cleaned up MediaService class, added support for Android Auto repository 2023-12-21 09:52:00 +01:00
antonio
1a407341ec feat: cleaned up MediaService class, added support for Android Auto repository 2023-12-21 09:51:48 +01:00
antonio
567350771c feat: implemented an embryonic version of Android Auto support 2023-12-21 09:49:39 +01:00
antonio
47bef77723 feat: declared Android Auto support 2023-12-21 09:47:39 +01:00
DelightLane
77d0b4182e feat: add korean localization 2023-12-21 11:36:57 +09:00
Romain Kowalski
caf64e0fa4 feat: Update French localization and fix previous errors 2023-12-18 13:23:07 +01:00
antonio
ac90b89099 fix: network security configuration 2023-12-12 21:57:53 +01:00
antonio
4523bb8e49 feat: implemented horizontal layout for music player 2023-12-12 21:28:49 +01:00
antonio
06f4898892 refactor/fix: renamed method name to be more descriptive and manually collapse bottomSheet on device state change 2023-12-12 21:28:02 +01:00
antonio
5bbab10485 fix: null checking 2023-12-12 21:24:52 +01:00
antonio
8fe66d058e gradle: build:gradle update 2023-12-10 17:32:03 +01:00
antonio
6a893ac424 Merge remote-tracking branch 'origin/main' 2023-11-29 19:22:41 +01:00
antonio
f184ace301 gradle: dependencies update 2023-11-29 19:22:25 +01:00
antonio
121c2b33da feat: added filter for songs that don't meet a user defined rating threshold 2023-11-29 19:21:42 +01:00
CappielloAntonio
215f1021d6 Merge pull request #108 from ixff/main
feat: add Simplified Chinese localization
2023-11-29 11:39:16 +01:00
ixff
ce6159ad93 feat: add Simplified Chinese localization 2023-11-27 23:08:31 +08:00
ixff
746ea93dbe chore: fix some typos 2023-11-27 22:54:24 +08:00
antonio
612c05fabc fix: increased button's tap area to facilitate easier clicking 2023-11-26 19:28:21 +01:00
43 changed files with 8721 additions and 218 deletions

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup JDK 17
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'

View File

@@ -28,8 +28,8 @@ android {
tempo {
dimension = "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 22
versionName '3.5.8'
versionCode 23
versionName '3.6.0'
}
notquitemy {
@@ -72,15 +72,15 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.room:room-runtime:2.6.0'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.appcompat:appcompat:1.6.1"
// Android Material
implementation 'com.google.android.material:material:1.10.0'
implementation 'com.google.android.material:material:1.11.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.16.0'
@@ -94,10 +94,10 @@ dependencies {
tempoImplementation 'androidx.media3:media3-cast:1.2.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

View File

@@ -0,0 +1,997 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "528d037bee0f0575f8e0670ae1b04e00",
"entities": [
{
"tableName": "queue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "trackOrder",
"columnName": "track_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastPlay",
"columnName": "last_play",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playingChanged",
"columnName": "playing_changed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"track_order"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverName",
"columnName": "server_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLowSecurity",
"columnName": "low_security",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recent_search",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))",
"fields": [
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"search"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "playlistName",
"columnName": "playlist_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "downloadState",
"columnName": "download_state",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "downloadUri",
"columnName": "download_uri",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "chronology",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "server",
"columnName": "server",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
"fields": [
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toStar",
"columnName": "toStar",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"timestamp"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "session_media_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '528d037bee0f0575f8e0670ae1b04e00')"
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,16 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locale_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme.SplashScreen"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
android:usesCleartextTraffic="true">
<!-- Declare that this session demo supports Android Auto. -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/auto_app_desc" />
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
@@ -46,7 +51,8 @@
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="androidx.media3.session.MediaBrowserService" />
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
</intent-filter>
</service>

View File

@@ -14,17 +14,19 @@ import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.database.dao.QueueDao;
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
import com.cappielloantonio.tempo.database.dao.ServerDao;
import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server;
import com.cappielloantonio.tempo.model.SessionMediaItem;
@Database(
version = 3,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class},
autoMigrations = {@AutoMigration(from = 2, to = 3)}
version = 8,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class},
autoMigrations = {@AutoMigration(from = 7, to = 8)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {
@@ -52,4 +54,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract ChronologyDao chronologyDao();
public abstract FavoriteDao favoriteDao();
public abstract SessionMediaItemDao sessionMediaItemDao();
}

View File

@@ -0,0 +1,29 @@
package com.cappielloantonio.tempo.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import java.util.List;
@Dao
public interface SessionMediaItemDao {
@Query("SELECT * FROM session_media_item WHERE id = :id")
SessionMediaItem get(String id);
@Query("SELECT * FROM session_media_item WHERE timestamp = :timestamp")
List<SessionMediaItem> get(long timestamp);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(SessionMediaItem sessionMediaItem);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insertAll(List<SessionMediaItem> sessionMediaItems);
@Query("DELETE FROM session_media_item")
void deleteAll();
}

View File

@@ -0,0 +1,281 @@
package com.cappielloantonio.tempo.model
import android.net.Uri
import android.os.Bundle
import androidx.annotation.Keep
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.glide.CustomGlideRequest
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.MusicUtil
import com.cappielloantonio.tempo.util.Preferences.getImageSize
import java.util.Date
@UnstableApi
@Keep
@Entity(tableName = "session_media_item")
class SessionMediaItem() {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "index")
var index: Int = 0
@ColumnInfo(name = "id")
var id: String? = null
@ColumnInfo(name = "parent_id")
var parentId: String? = null
@ColumnInfo(name = "is_dir")
var isDir: Boolean = false
@ColumnInfo
var title: String? = null
@ColumnInfo
var album: String? = null
@ColumnInfo
var artist: String? = null
@ColumnInfo
var track: Int? = null
@ColumnInfo
var year: Int? = null
@ColumnInfo
var genre: String? = null
@ColumnInfo(name = "cover_art_id")
var coverArtId: String? = null
@ColumnInfo
var size: Long? = null
@ColumnInfo(name = "content_type")
var contentType: String? = null
@ColumnInfo
var suffix: String? = null
@ColumnInfo("transcoding_content_type")
var transcodedContentType: String? = null
@ColumnInfo(name = "transcoded_suffix")
var transcodedSuffix: String? = null
@ColumnInfo
var duration: Int? = null
@ColumnInfo("bitrate")
var bitrate: Int? = null
@ColumnInfo
var path: String? = null
@ColumnInfo(name = "is_video")
var isVideo: Boolean = false
@ColumnInfo(name = "user_rating")
var userRating: Int? = null
@ColumnInfo(name = "average_rating")
var averageRating: Double? = null
@ColumnInfo(name = "play_count")
var playCount: Long? = null
@ColumnInfo(name = "disc_number")
var discNumber: Int? = null
@ColumnInfo
var created: Date? = null
@ColumnInfo
var starred: Date? = null
@ColumnInfo(name = "album_id")
var albumId: String? = null
@ColumnInfo(name = "artist_id")
var artistId: String? = null
@ColumnInfo
var type: String? = null
@ColumnInfo(name = "bookmark_position")
var bookmarkPosition: Long? = null
@ColumnInfo(name = "original_width")
var originalWidth: Int? = null
@ColumnInfo(name = "original_height")
var originalHeight: Int? = null
@ColumnInfo(name = "stream_id")
var streamId: String? = null
@ColumnInfo(name = "stream_url")
var streamUrl: String? = null
@ColumnInfo(name = "timestamp")
var timestamp: Long? = null
constructor(child: Child) : this() {
id = child.id
parentId = child.parentId
isDir = child.isDir
title = child.title
album = child.album
artist = child.artist
track = child.track
year = child.year
genre = child.genre
coverArtId = child.coverArtId
size = child.size
contentType = child.contentType
suffix = child.suffix
transcodedContentType = child.transcodedContentType
transcodedSuffix = child.transcodedSuffix
duration = child.duration
bitrate = child.bitrate
path = child.path
isVideo = child.isVideo
userRating = child.userRating
averageRating = child.averageRating
playCount = child.playCount
discNumber = child.discNumber
created = child.created
starred = child.starred
albumId = child.albumId
artistId = child.artistId
type = Constants.MEDIA_TYPE_MUSIC
bookmarkPosition = child.bookmarkPosition
originalWidth = child.originalWidth
originalHeight = child.originalHeight
}
constructor(podcastEpisode: PodcastEpisode) : this() {
id = podcastEpisode.id
parentId = podcastEpisode.parentId
isDir = podcastEpisode.isDir
title = podcastEpisode.title
album = podcastEpisode.album
artist = podcastEpisode.artist
year = podcastEpisode.year
genre = podcastEpisode.genre
coverArtId = podcastEpisode.coverArtId
size = podcastEpisode.size
contentType = podcastEpisode.contentType
suffix = podcastEpisode.suffix
duration = podcastEpisode.duration
bitrate = podcastEpisode.bitrate
path = podcastEpisode.path
isVideo = podcastEpisode.isVideo
created = podcastEpisode.created
artistId = podcastEpisode.artistId
streamId = podcastEpisode.streamId
type = Constants.MEDIA_TYPE_PODCAST
}
constructor(internetRadioStation: InternetRadioStation) : this() {
id = internetRadioStation.id
title = internetRadioStation.name
streamUrl = internetRadioStation.streamUrl
type = Constants.MEDIA_TYPE_RADIO
}
fun getMediaItem(): MediaItem {
val uri: Uri = getStreamUri()
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
val bundle = Bundle()
bundle.putString("id", id)
bundle.putString("parentId", parentId)
bundle.putBoolean("isDir", isDir)
bundle.putString("title", title)
bundle.putString("album", album)
bundle.putString("artist", artist)
bundle.putInt("track", track ?: 0)
bundle.putInt("year", year ?: 0)
bundle.putString("genre", genre)
bundle.putString("coverArtId", coverArtId)
bundle.putLong("size", size ?: 0)
bundle.putString("contentType", contentType)
bundle.putString("suffix", suffix)
bundle.putString("transcodedContentType", transcodedContentType)
bundle.putString("transcodedSuffix", transcodedSuffix)
bundle.putInt("duration", duration ?: 0)
bundle.putInt("bitrate", bitrate ?: 0)
bundle.putString("path", path)
bundle.putBoolean("isVideo", isVideo)
bundle.putInt("userRating", userRating ?: 0)
bundle.putDouble("averageRating", averageRating ?: .0)
bundle.putLong("playCount", playCount ?: 0)
bundle.putInt("discNumber", discNumber ?: 0)
bundle.putLong("created", created?.time ?: 0)
bundle.putLong("starred", starred?.time ?: 0)
bundle.putString("albumId", albumId)
bundle.putString("artistId", artistId)
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC)
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
bundle.putInt("originalWidth", originalWidth ?: 0)
bundle.putInt("originalHeight", originalHeight ?: 0)
bundle.putString("uri", uri.toString())
return MediaItem.Builder()
.setMediaId(id!!)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(title))
.setTrackNumber(track ?: 0)
.setDiscNumber(discNumber ?: 0)
.setReleaseYear(year ?: 0)
.setAlbumTitle(MusicUtil.getReadableString(album))
.setArtist(MusicUtil.getReadableString(artist))
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(bundle)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build()
}
private fun getStreamUri(): Uri {
return when (type) {
Constants.MEDIA_TYPE_MUSIC -> {
MusicUtil.getStreamUri(id)
}
Constants.MEDIA_TYPE_PODCAST -> {
MusicUtil.getStreamUri(streamId)
}
Constants.MEDIA_TYPE_RADIO -> {
Uri.parse(streamUrl)
}
else -> {
MusicUtil.getStreamUri(id)
}
}
}
}

View File

@@ -0,0 +1,957 @@
package com.cappielloantonio.tempo.repository;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.LibraryResult;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Directory;
import com.cappielloantonio.tempo.subsonic.models.Index;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AutomotiveRepository {
private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao();
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getAlbums(String prefix, String type, int size) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getAlbumList2(type, size, 0, null, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
.setAlbumTitle(album.getName())
.setArtist(album.getArtist())
.setGenre(album.getGenre())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + album.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredSongs() {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getStarred2()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getSongs() != null) {
List<Child> songs = response.body().getSubsonicResponse().getStarred2().getSongs();
setChildrenMetadata(songs);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(songs);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredAlbums(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getStarred2()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getAlbums() != null) {
List<AlbumID3> albums = response.body().getSubsonicResponse().getStarred2().getAlbums();
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
.setArtist(album.getArtist())
.setGenre(album.getGenre())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + album.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredArtists(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getStarred2()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getArtists() != null) {
List<ArtistID3> artists = response.body().getSubsonicResponse().getStarred2().getArtists();
Collections.shuffle(artists);
List<MediaItem> mediaItems = new ArrayList<>();
for (ArtistID3 artist : artists) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + artist.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getMusicFolders(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getMusicFolders()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null && response.body().getSubsonicResponse().getMusicFolders().getMusicFolders() != null) {
List<MusicFolder> musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders();
List<MediaItem> mediaItems = new ArrayList<>();
for (MusicFolder musicFolder : musicFolders) {
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(musicFolder.getName())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + musicFolder.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getIndexes(String prefix, String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getIndexes(id, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) {
List<MediaItem> mediaItems = new ArrayList<>();
if (response.body().getSubsonicResponse().getIndexes().getIndices() != null) {
List<Index> indices = response.body().getSubsonicResponse().getIndexes().getIndices();
for (Index index : indices) {
if (index.getArtists() != null) {
for (Artist artist : index.getArtists()) {
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_ARTIST)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + artist.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
}
}
}
if (response.body().getSubsonicResponse().getIndexes().getChildren() != null) {
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
for (Child song : children) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(song.getTitle())
.setAlbumTitle(song.getAlbum())
.setArtist(song.getArtist())
.setIsBrowsable(false)
.setIsPlayable(true)
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + song.getId())
.setMediaMetadata(mediaMetadata)
.setUri(MusicUtil.getStreamUri(song.getId()))
.build();
mediaItems.add(mediaItem);
}
setChildrenMetadata(children);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getDirectories(String prefix, String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getMusicDirectory(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null && response.body().getSubsonicResponse().getDirectory().getChildren() != null) {
Directory directory = response.body().getSubsonicResponse().getDirectory();
List<MediaItem> mediaItems = new ArrayList<>();
for (Child child : directory.getChildren()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(child.getTitle())
.setIsBrowsable(child.isDir())
.setIsPlayable(!child.isDir())
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(child.isDir() ? prefix + child.getId() : child.getId())
.setMediaMetadata(mediaMetadata)
.setUri(!child.isDir() ? MusicUtil.getStreamUri(child.getId()) : Uri.parse(""))
.build();
mediaItems.add(mediaItem);
}
setChildrenMetadata(directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList()));
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getPlaylists(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.getPlaylists()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null && response.body().getSubsonicResponse().getPlaylists().getPlaylists() != null) {
List<Playlist> playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
List<MediaItem> mediaItems = new ArrayList<>();
for (Playlist playlist : playlists) {
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(playlist.getName())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + playlist.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getNewestPodcastEpisodes(int count) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getPodcastClient()
.getNewestPodcasts(count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getNewestPodcasts() != null && response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes() != null) {
List<PodcastEpisode> episodes = response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes();
List<MediaItem> mediaItems = new ArrayList<>();
for (PodcastEpisode episode : episodes) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(episode.getTitle())
.setIsBrowsable(false)
.setIsPlayable(true)
.setMediaType(MediaMetadata.MEDIA_TYPE_PODCAST_EPISODE)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(episode.getId())
.setMediaMetadata(mediaMetadata)
.setUri(MusicUtil.getStreamUri(episode.getStreamId()))
.build();
mediaItems.add(mediaItem);
}
setPodcastEpisodesMetadata(episodes);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getInternetRadioStations() {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getInternetRadioClient()
.getInternetRadioStations()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getInternetRadioStations() != null && response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations() != null) {
List<InternetRadioStation> radioStations = response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations();
List<MediaItem> mediaItems = new ArrayList<>();
for (InternetRadioStation radioStation : radioStations) {
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(radioStation.getName())
.setIsBrowsable(false)
.setIsPlayable(true)
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(radioStation.getId())
.setMediaMetadata(mediaMetadata)
.setUri(radioStation.getStreamUrl())
.build();
mediaItems.add(mediaItem);
}
setInternetRadioStationsMetadata(radioStations);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getAlbumTracks(String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getAlbum(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null && response.body().getSubsonicResponse().getAlbum().getSongs() != null) {
List<Child> tracks = response.body().getSubsonicResponse().getAlbum().getSongs();
setChildrenMetadata(tracks);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(tracks);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getArtistAlbum(String prefix, String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getArtist(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
List<MediaItem> mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
.setAlbumTitle(album.getName())
.setArtist(album.getArtist())
.setGenre(album.getGenre())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(prefix + album.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getPlaylistSongs(String id) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.getPlaylist(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null && response.body().getSubsonicResponse().getPlaylist().getEntries() != null) {
List<Child> tracks = response.body().getSubsonicResponse().getPlaylist().getEntries();
setChildrenMetadata(tracks);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(tracks);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getMadeForYou(String id, int count) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null && response.body().getSubsonicResponse().getSimilarSongs2().getSongs() != null) {
List<Child> tracks = response.body().getSubsonicResponse().getSimilarSongs2().getSongs();
setChildrenMetadata(tracks);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(tracks);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> search(String query, String albumPrefix, String artistPrefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getSearchingClient()
.search3(query, 20, 20, 20)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSearchResult3() != null) {
List<MediaItem> mediaItems = new ArrayList<>();
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(artistPrefix + artist.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
}
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
.setAlbumTitle(album.getName())
.setArtist(album.getArtist())
.setGenre(album.getGenre())
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM)
.setArtworkUri(artworkUri)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(albumPrefix + album.getId())
.setMediaMetadata(mediaMetadata)
.setUri("")
.build();
mediaItems.add(mediaItem);
}
}
if (response.body().getSubsonicResponse().getSearchResult3().getSongs() != null) {
List<Child> tracks = response.body().getSubsonicResponse().getSearchResult3().getSongs();
setChildrenMetadata(tracks);
mediaItems.addAll(MappingUtil.mapMediaItems(tracks));
}
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
@OptIn(markerClass = UnstableApi.class)
public void setChildrenMetadata(List<Child> children) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> sessionMediaItems = new ArrayList<>();
for (Child child : children) {
SessionMediaItem sessionMediaItem = new SessionMediaItem(child);
sessionMediaItem.setTimestamp(timestamp);
sessionMediaItems.add(sessionMediaItem);
}
InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems);
Thread thread = new Thread(insertAll);
thread.start();
}
@OptIn(markerClass = UnstableApi.class)
public void setPodcastEpisodesMetadata(List<PodcastEpisode> podcastEpisodes) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> sessionMediaItems = new ArrayList<>();
for (PodcastEpisode podcastEpisode : podcastEpisodes) {
SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisode);
sessionMediaItem.setTimestamp(timestamp);
sessionMediaItems.add(sessionMediaItem);
}
InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems);
Thread thread = new Thread(insertAll);
thread.start();
}
@OptIn(markerClass = UnstableApi.class)
public void setInternetRadioStationsMetadata(List<InternetRadioStation> internetRadioStations) {
long timestamp = System.currentTimeMillis();
ArrayList<SessionMediaItem> sessionMediaItems = new ArrayList<>();
for (InternetRadioStation internetRadioStation : internetRadioStations) {
SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStation);
sessionMediaItem.setTimestamp(timestamp);
sessionMediaItems.add(sessionMediaItem);
}
InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems);
Thread thread = new Thread(insertAll);
thread.start();
}
public SessionMediaItem getSessionMediaItem(String id) {
SessionMediaItem sessionMediaItem = null;
GetMediaItemThreadSafe getMediaItemThreadSafe = new GetMediaItemThreadSafe(sessionMediaItemDao, id);
Thread thread = new Thread(getMediaItemThreadSafe);
thread.start();
try {
thread.join();
sessionMediaItem = getMediaItemThreadSafe.getSessionMediaItem();
} catch (InterruptedException e) {
e.printStackTrace();
}
return sessionMediaItem;
}
public List<MediaItem> getMetadatas(long timestamp) {
List<MediaItem> mediaItems = Collections.emptyList();
GetMediaItemsThreadSafe getMediaItemsThreadSafe = new GetMediaItemsThreadSafe(sessionMediaItemDao, timestamp);
Thread thread = new Thread(getMediaItemsThreadSafe);
thread.start();
try {
thread.join();
mediaItems = getMediaItemsThreadSafe.getMediaItems();
} catch (InterruptedException e) {
e.printStackTrace();
}
return mediaItems;
}
public void deleteMetadata() {
DeleteAllThreadSafe delete = new DeleteAllThreadSafe(sessionMediaItemDao);
Thread thread = new Thread(delete);
thread.start();
}
private static class GetMediaItemThreadSafe implements Runnable {
private final SessionMediaItemDao sessionMediaItemDao;
private final String id;
private SessionMediaItem sessionMediaItem;
public GetMediaItemThreadSafe(SessionMediaItemDao sessionMediaItemDao, String id) {
this.sessionMediaItemDao = sessionMediaItemDao;
this.id = id;
}
@Override
public void run() {
sessionMediaItem = sessionMediaItemDao.get(id);
}
public SessionMediaItem getSessionMediaItem() {
return sessionMediaItem;
}
}
@OptIn(markerClass = UnstableApi.class)
private static class GetMediaItemsThreadSafe implements Runnable {
private final SessionMediaItemDao sessionMediaItemDao;
private final Long timestamp;
private final List<MediaItem> mediaItems = new ArrayList<>();
public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) {
this.sessionMediaItemDao = sessionMediaItemDao;
this.timestamp = timestamp;
}
@Override
public void run() {
List<SessionMediaItem> sessionMediaItems = sessionMediaItemDao.get(timestamp);
sessionMediaItems.forEach(sessionMediaItem -> mediaItems.add(sessionMediaItem.getMediaItem()));
}
public List<MediaItem> getMediaItems() {
return mediaItems;
}
}
private static class InsertAllThreadSafe implements Runnable {
private final SessionMediaItemDao sessionMediaItemDao;
private final List<SessionMediaItem> sessionMediaItems;
public InsertAllThreadSafe(SessionMediaItemDao sessionMediaItemDao, List<SessionMediaItem> sessionMediaItems) {
this.sessionMediaItemDao = sessionMediaItemDao;
this.sessionMediaItems = sessionMediaItems;
}
@Override
public void run() {
sessionMediaItemDao.insertAll(sessionMediaItems);
}
}
private static class DeleteAllThreadSafe implements Runnable {
private final SessionMediaItemDao sessionMediaItemDao;
public DeleteAllThreadSafe(SessionMediaItemDao sessionMediaItemDao) {
this.sessionMediaItemDao = sessionMediaItemDao;
}
@Override
public void run() {
sessionMediaItemDao.deleteAll();
}
}
}

View File

@@ -1,7 +1,5 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;

View File

@@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class MediaLibraryScanningClient {
private static final String TAG = "SystemClient";
private static final String TAG = "MediaLibraryScanningClient";
private final Subsonic subsonic;
private final MediaLibraryScanningService mediaLibraryScanningService;

View File

@@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class PodcastClient {
private static final String TAG = "SystemClient";
private static final String TAG = "PodcastClient";
private final Subsonic subsonic;
private final PodcastService podcastService;

View File

@@ -94,7 +94,7 @@ public class MainActivity extends BaseActivity {
@Override
public void onBackPressed() {
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
collapseBottomSheet();
collapseBottomSheetDelayed();
else
super.onBackPressed();
}
@@ -118,7 +118,7 @@ public class MainActivity extends BaseActivity {
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit();
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
checkBottomSheetAfterStateChanged();
}
public void setBottomSheetInPeek(Boolean isVisible) {
@@ -137,7 +137,13 @@ public class MainActivity extends BaseActivity {
}
}
public void collapseBottomSheet() {
private void checkBottomSheetAfterStateChanged() {
final Handler handler = new Handler();
final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded());
handler.postDelayed(runnable, 100);
}
public void collapseBottomSheetDelayed() {
final Handler handler = new Handler();
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
handler.postDelayed(runnable, 100);

View File

@@ -51,6 +51,7 @@ import com.cappielloantonio.tempo.ui.adapter.YearAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
@@ -153,6 +154,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.discoveryTextViewClickable.setOnClickListener(v -> {
homeViewModel.getRandomShuffleSample().observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
@@ -306,6 +309,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.discoverSongViewPager.setAdapter(discoverSongAdapter);
bind.discoverSongViewPager.setOffscreenPageLimit(1);
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs == null) {
if (bind != null)
bind.homeDiscoveryPlaceholder.placeholder.setVisibility(View.VISIBLE);
@@ -330,6 +335,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
similarMusicAdapter = new SimilarTrackAdapter(this);
bind.similarTracksRecyclerView.setAdapter(similarMusicAdapter);
homeViewModel.getStarredTracksSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs == null) {
if (bind != null)
bind.homeSimilarTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
@@ -744,6 +751,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
if (mediaBrowserListenableFuture != null) {
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs != null && songs.size() > 0) {
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
}
@@ -783,6 +792,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
if (mediaBrowserListenableFuture != null) {
homeViewModel.getArtistInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
@@ -792,6 +803,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
} else if (bundle.containsKey(Constants.MEDIA_BEST_OF) && bundle.getBoolean(Constants.MEDIA_BEST_OF)) {
if (mediaBrowserListenableFuture != null) {
homeViewModel.getArtistBestOf(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);

View File

@@ -56,6 +56,7 @@ public class PlayerControllerFragment extends Fragment {
private Chip playerMediaExtension;
private TextView playerMediaBitrate;
private ConstraintLayout playerQuickActionView;
private ImageButton playerOpenQueueButton;
private ImageButton playerTrackInfo;
private MainActivity activity;
@@ -109,13 +110,14 @@ public class PlayerControllerFragment extends Fragment {
playerMediaExtension = bind.getRoot().findViewById(R.id.player_media_extension);
playerMediaBitrate = bind.getRoot().findViewById(R.id.player_media_bitrate);
playerQuickActionView = bind.getRoot().findViewById(R.id.player_quick_action_view);
playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button);
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
}
private void initQuickActionView() {
playerQuickActionView.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8));
playerQuickActionView.setOnClickListener(view -> {
playerOpenQueueButton.setOnClickListener(view -> {
PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.goToQueuePage();
@@ -294,7 +296,7 @@ public class PlayerControllerFragment extends Fragment {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.ARTIST_OBJECT, artist);
NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle);
activity.collapseBottomSheet();
activity.collapseBottomSheetDelayed();
});
}
});

View File

@@ -80,7 +80,10 @@ public class PlayerCoverFragment extends Fragment {
handler.removeCallbacksAndMessages(null);
final Runnable runnable = () -> bind.nowPlayingTapButton.setVisibility(View.GONE);
final Runnable runnable = () -> {
if (bind != null) bind.nowPlayingTapButton.setVisibility(View.GONE);
};
handler.postDelayed(runnable, 10000);
}

View File

@@ -115,6 +115,8 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
@Override
public void onLoadMedia(List<?> media) {
MusicUtil.ratingFilter((ArrayList<Child>) media);
if (media.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
((MainActivity) requireActivity()).setBottomSheetInPeek(true);

View File

@@ -89,6 +89,8 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
ArtistRepository artistRepository = new ArtistRepository();
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
@@ -102,6 +104,8 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
playRandom.setOnClickListener(v -> {
ArtistRepository artistRepository = new ArtistRepository();
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
((MainActivity) requireActivity()).setBottomSheetInPeek(true);

View File

@@ -114,6 +114,8 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs == null) {
dismissBottomSheet();
return;

View File

@@ -82,6 +82,8 @@ public class MappingUtil {
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
@@ -116,6 +118,8 @@ public class MappingUtil {
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
@@ -145,6 +149,8 @@ public class MappingUtil {
.setTitle(internetRadioStation.getName())
.setArtist(internetRadioStation.getStreamUrl())
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
@@ -193,6 +199,8 @@ public class MappingUtil {
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
@@ -201,12 +209,6 @@ public class MappingUtil {
.setExtras(bundle)
.build()
)
/* .setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(0)
.setEndPositionMs(podcastEpisode.getDuration() * 1000)
.build()
) */
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();

View File

@@ -307,4 +307,17 @@ public class MusicUtil {
private static ConnectivityManager getConnectivityManager() {
return (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
}
public static void ratingFilter(List<Child> toFilter) {
if (toFilter == null || toFilter.isEmpty()) return;
List<Child> filtered = toFilter
.stream()
.filter(child -> (child.getUserRating() != null && child.getUserRating() >= Preferences.getMinStarRatingAccepted()) || (child.getUserRating() == null))
.collect(Collectors.toList());
toFilter.clear();
toFilter.addAll(filtered);
}
}

View File

@@ -43,6 +43,9 @@ object Preferences {
private const val SCROBBLING = "scrobbling"
private const val ESTIMATE_CONTENT_LENGTH = "estimate_content_length"
private const val BUFFERING_STRATEGY = "buffering_strategy"
private const val SKIP_MIN_STAR_RATING = "skip_min_star_rating"
private const val MIN_STAR_RATING = "min_star_rating"
@JvmStatic
fun getServer(): String? {
@@ -338,4 +341,8 @@ object Preferences {
return App.getInstance().preferences.getString(BUFFERING_STRATEGY, "1")!!.toDouble()
}
@JvmStatic
fun getMinStarRatingAccepted(): Int {
return App.getInstance().preferences.getInt(MIN_STAR_RATING, 0)
}
}

View File

@@ -0,0 +1,359 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/now_playing_media_controller_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.45" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_media_quality_sector"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.chip.Chip
android:id="@+id/player_media_extension"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:clickable="false"
android:text="Unknown"
app:chipStrokeWidth="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/player_media_bitrate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"/>
<TextView
android:id="@+id/player_media_bitrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintTop_toTopOf="@id/player_media_extension"
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
app:layout_constraintStart_toEndOf="@id/player_media_extension"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageButton
android:id="@+id/player_info_track"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/player_media_extension"
app:layout_constraintBottom_toBottomOf="@id/player_media_extension"
app:srcCompat="@drawable/ic_info_stream"
app:tint="?attr/colorOnPrimaryContainer" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/player_media_cover_view_pager"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/vertical_guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/player_media_title_label"
style="@style/HeadlineLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@+id/player_artist_name_label"
app:layout_constraintEnd_toStartOf="@+id/button_favorite"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector" />
<TextView
android:id="@+id/player_artist_name_label"
style="@style/TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@+id/button_favorite"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/player_media_title_label"
app:layout_constraintBottom_toTopOf="@id/exo_progress"/>
<ToggleButton
android:id="@+id/button_favorite"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_favorite_selector"
android:checked="false"
android:text=""
android:textOff=""
android:textOn=""
app:layout_constraintBottom_toBottomOf="@+id/player_media_title_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/player_media_title_label" />
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="24dp"
android:paddingBottom="4dp"
android:text="@string/label_placeholder"
android:textColor="@color/titleTextColor"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/exo_progress" />
<androidx.media3.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
app:bar_height="2dp"
app:buffered_color="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/player_artist_name_label"
app:layout_constraintBottom_toTopOf="@+id/player_play_pause_placeholder_view"
app:played_color="?attr/colorOnPrimaryContainer"
app:scrubber_color="?attr/colorOnPrimaryContainer"
app:unplayed_color="?attr/colorPrimaryContainer" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="24dp"
android:paddingBottom="4dp"
android:text="@string/label_placeholder"
android:textColor="@color/titleTextColor"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/exo_progress" />
<View
android:id="@+id/placeholder_view_left"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left" />
<View
android:id="@+id/placeholder_view_middle_left"
android:layout_width="42dp"
android:layout_height="42dp"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintStart_toEndOf="@id/placeholder_view_left"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view" />
<View
android:id="@+id/player_play_pause_placeholder_view"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="36dp"
app:layout_constraintBottom_toTopOf="@+id/player_quick_action_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toBottomOf="@+id/exo_progress"/>
<View
android:id="@+id/placeholder_view_middle_right"
android:layout_width="42dp"
android:layout_height="42dp"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@id/placeholder_view_right"
app:layout_constraintStart_toEndOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view" />
<View
android:id="@+id/placeholder_view_right"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="24dp"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right" />
<Button
android:id="@+id/player_playback_speed_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_shuffle"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
app:srcCompat="@drawable/ic_shuffle"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_rew"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
android:src="@drawable/ic_replay"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintStart_toEndOf="@id/placeholder_view_left"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_prev"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintStart_toEndOf="@id/placeholder_view_left"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view"
app:srcCompat="@drawable/ic_skip_previous"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@id/exo_play_pause"
style="@style/ExoStyledControls.Button.Center.PlayPause"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toEndOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintStart_toStartOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_next"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@id/placeholder_view_right"
app:layout_constraintStart_toEndOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view"
app:srcCompat="@drawable/ic_skip_next"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_ffwd"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintEnd_toStartOf="@id/placeholder_view_right"
app:layout_constraintStart_toEndOf="@+id/player_play_pause_placeholder_view"
app:layout_constraintTop_toTopOf="@+id/player_play_pause_placeholder_view"
app:srcCompat="@drawable/ic_forward"
app:tint="?attr/colorOnPrimaryContainer" />
<ImageButton
android:id="@+id/exo_repeat_toggle"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right"
app:srcCompat="@drawable/ic_repeat"
app:tint="?attr/colorOnPrimaryContainer" />
<ToggleButton
android:id="@+id/player_skip_silence_toggle_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_skip_silence_selector"
android:text=""
android:textOff=""
android:textOn=""
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right"
app:tint="?attr/colorOnPrimaryContainer" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_quick_action_view"
android:layout_width="0dp"
android:layout_height="@dimen/now_playing_bottom_peek_height"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone">
<ImageButton
android:id="@+id/player_open_queue_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/ic_queue" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -344,8 +344,9 @@
<ImageButton
android:id="@+id/player_open_queue_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -211,4 +211,32 @@
<item>2</item>
<item>3</item>
</string-array>
<string-array name="buffering_strategy_titles">
<item>Minimum</item>
<item>Modérée</item>
<item>Agressive</item>
<item>Extrême</item>
</string-array>
<string-array name="buffering_strategy_values">
<item>.1</item>
<item>1</item>
<item>4</item>
<item>8</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>0 étoile minimum</item>
<item>1 étoile minimum</item>
<item>2 étoiles minimum</item>
<item>3 étoiles minimum</item>
<item>4 étoiles minimum</item>
</string-array>
<string-array name="skip_min_star_rating_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
</resources>

View File

@@ -57,11 +57,15 @@
<string name="delete_download_storage_dialog_summary">Sachez que la poursuite de cette action entraînera la suppression permanente de tous les éléments sauvegardés et téléchargés à partir de tous les serveurs</string>
<string name="delete_download_storage_dialog_title">Supprimer les éléments téléchargés</string>
<string name="description_empty_title">Aucune description disponible</string>
<string name="download_directory_dialog_negative_button">Annuler</string>
<string name="download_directory_dialog_positive_button">Télécharger</string>
<string name="download_directory_dialog_summary">Toutes les pistes dans ce dossier seront téléchargées. Les pistes dans les sous-dossiers ne seront pas téléchargées.</string>
<string name="download_directory_dialog_title">Télécharger toutes les pistes.</string>
<string name="download_info_empty_subtitle">Dès que vous téléchargerez une musique, vous la trouverez ici</string>
<string name="download_info_empty_title">Aucun téléchargement pour l\'instant</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s éléments</string>
<string name="download_item_single_subtitle_formatter">%1$s éléments</string>
<string name="download_storage_dialog_sub_summary">Redémarrez l\'app pour appliquer les changements.</string>
<string name="download_storage_dialog_sub_summary">Redémarrez l\'application pour appliquer les changements.</string>
<string name="download_storage_dialog_summary">Changer la destination des téléchargements d\'un espace de stockage à un autre résultera en la suppression immédiate de tous les fichiers précédemment téléchargés dans l\'autre espace de stockage.</string>
<string name="download_storage_dialog_title">Sélectionnez l\'option de stockage</string>
<string name="download_storage_external_dialog_positive_button">Externe</string>
@@ -147,6 +151,7 @@
<string name="menu_sort_random">Aléatoire</string>
<string name="menu_sort_year">Année</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">Vider la file d\'attente</string>
<string name="player_server_priority">Priorité serveur</string>
<string name="playlist_catalogue_title">Catalogue des Playlists</string>
<string name="playlist_catalogue_title_expanded">Parcourir les playlists</string>
@@ -229,8 +234,10 @@
<string name="settings_audio_transcode_priority_summary">Si activé, Tempo ne forcera pas le streaming des pistes avec les paramètres ci-dessous.</string>
<string name="settings_audio_transcode_priority_title">Prioriser les paramètres de transcodage du serveur</string>
<string name="settings_audio_transcode_priority_toast">La priorité au transcodage de la piste est donnée au serveur</string>
<string name="settings_buffering_strategy">Stratégie de mise en mémoire tampon</string>
<string name="settings_buffering_strategy_summary">Redémarrez l\'application pour appliquer les changements.</string>
<string name="settings_covers_cache">Taille du cache des illustrations</string>
<string name="settings_data_saving_mode_summary">Pour réduire la consommation de données, évitez de télécharger les illustrations.</string>
<string name="settings_data_saving_mode_summary">Pour réduire la consommation de données, éviter de télécharger les illustrations.</string>
<string name="settings_data_saving_mode_title">Limiter l\'utilisation des données mobiles</string>
<string name="settings_delete_download_storage_summary">Continuer entraînera la suppression irréversible de tous les éléments sauvegardés.</string>
<string name="settings_delete_download_storage_title">Supprimer les éléments sauvegardés</string>
@@ -261,20 +268,28 @@
<string name="settings_rounded_corner_size">Taille des arrondis</string>
<string name="settings_rounded_corner_size_summary">Définit l\'ampleur de l\'angle de courbure.</string>
<string name="settings_rounded_corner_summary">Si activé, arrondi les angles des illustrations. Les modifications prendront effet au redémarrage.</string>
<string name="settings_scan_title">Scanner la librairie</string>
<string name="settings_scan_title">Scanner la bibliothèque</string>
<string name="settings_scrobble_title">Activer le scrobbling</string>
<string name="settings_share_title">Activer le partage de musique</string>
<string name="settings_sub_summary_scrobble">À noter que le scrobbling doit être activé sur le serveur pour qu\'il puisse recevoir ces données</string>
<string name="settings_summary_skip_min_star_rating">Lors de l\'écoute de la radio d\'un artiste, d\'un mix instantané ou de tout la bibliothèque en aléatoire, les pistes en dessous d\'une certaine note seront ignorées.</string>
<string name="settings_summary_replay_gain">Le Replay Gain est une fonctionnalité qui vous permet d\'ajuster le volume des pistes audio pour une expérience d\'écoute cohérente. Fonctionne uniquement si la piste contient les métadonnées nécessaires.</string>
<string name="settings_summary_scrobble">Le scrobbling permet à votre appareil d\'envoyer des informations sur les musiques que vous écoutez au serveur afin de créer des recommendations personnalisées basées sur vos préférences musicales.</string>
<string name="settings_summary_share">Permet à l\'utilisateur de partager de la musique via un lien. Cette fonctionnalité doit être supportée et activée sur le serveur et est limitée aux pistes, albums et playlists individuellement.</string>
<string name="settings_summary_syncing">Renvoie l\'état de la file d\'attente de cet utilisateur. Cela inclut les pistes dans la file, la piste actuellement écoutée et la position dans la piste. Cette fonctionnalité doit être supportée par le serveur.</string>
<string name="settings_summary_transcoding">Le mode de transcodage à prioriser. Si reglé sur \"Lecture directe\", le bitrate du fichier ne sera pas modifié.</string>
<string name="settings_summary_transcoding_download">Télécharge les médias transcodés. Si activé, les paramètres de transcodage suivants seront utilisés pour les téléchargements.\n\n Si le format de transcodage est reglé à \"Téléchargement direct\", le bitrate du fichier ne sera pas modifé.</string>
<string name="settings_summary_transcoding_estimate_content_length">Quand le fichier est transcodé à la volé, en général, le client n\'affiche pas la durée de la piste. Il est possible de demander aux serveurs qui le supportent d\'estimer la durée de la piste écoutée, mais les temps de réponses peuvent être plus longs.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si activés, les pistes favorites seront téléchargées pour l\'écoute hors-ligne</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">Si activé, les pistes favorites seront téléchargées pour l\'écoute hors-ligne</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Synchronisation des pistes favorites pour écoute hors-ligne</string>
<string name="settings_theme">Thème</string>
<string name="settings_title_data">Données</string>
<string name="settings_title_general">Géneral</string>
<string name="settings_title_rating">Note</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_scrobble">Scrobble</string>
<string name="settings_title_skip_min_star_rating">Ignorer des musiques selon leur note</string>
<string name="settings_title_skip_min_star_rating_dialog">Musiques avec une note de:</string>
<string name="settings_title_share">Partage</string>
<string name="settings_title_syncing">Synchronisation</string>
<string name="settings_title_transcoding">Transcodage</string>

View File

@@ -0,0 +1,242 @@
<resources>
<string-array name="theme_list_titles">
<item>라이트</item>
<item>다크</item>
<item>시스템 기본</item>
</string-array>
<string-array name="theme_list_values">
<item>light</item>
<item>dark</item>
<item>default</item>
</string-array>
<string-array name="pref_cache_size_titles">
<item>높음</item>
<item>중간</item>
<item>낮음</item>
</string-array>
<string-array name="pref_cache_size_values">
<item>500</item>
<item>250</item>
<item>125</item>
</string-array>
<string-array name="pref_image_size_titles">
<item>높음</item>
<item>중간</item>
<item>낮음</item>
</string-array>
<string-array name="pref_image_size_values">
<item>-1</item>
<item>500</item>
<item>300</item>
</string-array>
<string-array name="max_bitrate_wifi_list_titles">
<item>원본</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_wifi_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="max_bitrate_mobile_list_titles">
<item>원본</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_mobile_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="max_bitrate_download_list_titles">
<item>원본</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_download_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_titles">
<item>직접 재생</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_titles">
<item>직접 재생</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_titles">
<item>직접 다운로드</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="queue_syncing_countdown_titles">
<item>10초</item>
<item>5초</item>
<item>2초</item>
</string-array>
<string-array name="queue_syncing_countdown_values">
<item>10</item>
<item>5</item>
<item>2</item>
</string-array>
<string-array name="rounded_corner_size_titles">
<item>높음</item>
<item>중간</item>
<item>낮음</item>
</string-array>
<string-array name="rounded_corner_size_values">
<item>18</item>
<item>12</item>
<item>6</item>
</string-array>
<string-array name="replay_gain_titles">
<item>비활성</item>
<item>트랙</item>
<item>앨범</item>
<item>자동</item>
</string-array>
<string-array name="replay_gain_values">
<item>disabled</item>
<item>track</item>
<item>album</item>
<item>auto</item>
</string-array>
<string-array name="transcoded_download_option_list_titles">
<item>트랜스코딩 하지 않음</item>
<item>서버 셋팅</item>
<item>Wi-Fi 트랜스코딩 포맷</item>
<item>모바일 트랜스코딩 포맷</item>
</string-array>
<string-array name="transcoded_download_option_list_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="buffering_strategy_titles">
<item>최소한</item>
<item>보통</item>
<item>적극적</item>
<item>최대한</item>
</string-array>
<string-array name="buffering_strategy_values">
<item>.1</item>
<item>1</item>
<item>4</item>
<item>8</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>최소 별점 0</item>
<item>최소 별점 1</item>
<item>최소 별점 2</item>
<item>최소 별점 3</item>
<item>최소 별점 4</item>
</string-array>
<string-array name="skip_min_star_rating_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
</resources>

View File

@@ -0,0 +1,366 @@
<resources>
<string name="activity_battery_optimizations_conclusion">문제가 있을 시 https://dontkillmyapp.com에 방문해 주세요. 앱 성능에 영향을 줄 수 있는 절전 기능을 비활성화하는 방법에 대한 자세한 설명을 찾을 수 있습니다.</string>
<string name="activity_battery_optimizations_summary">화면이 꺼진 상태에서 음악 재생을 하기 위해서는 배터리 최적화를 비활성화 해주세요.</string>
<string name="activity_battery_optimizations_title">배터리 최적화</string>
<string name="activity_info_offline_mode">오프라인 모드</string>
<string name="album_bottom_sheet_add_to_queue">재생목록에 추가</string>
<string name="album_bottom_sheet_download_all">모두 다운로드</string>
<string name="album_bottom_sheet_go_to_artist">아티스트로 이동</string>
<string name="album_bottom_sheet_instant_mix">인스턴트 믹스</string>
<string name="album_bottom_sheet_play_next">다음 재생</string>
<string name="album_bottom_sheet_remove_all">모두 제거</string>
<string name="album_bottom_sheet_share">공유</string>
<string name="album_bottom_sheet_shuffle">셔플</string>
<string name="album_catalogue_title">앨범</string>
<string name="album_catalogue_title_expanded">앨범 찾아보기</string>
<string name="album_error_retrieving_artist">아티스트를 검색하던 중 오류가 발생했습니다.</string>
<string name="album_list_page_downloaded">다운로드한 앨범</string>
<string name="album_list_page_most_played">가장 많이 재생한 앨범</string>
<string name="album_list_page_new_releases">New releases</string>
<string name="album_list_page_recently_added">최근 추가한 앨범</string>
<string name="album_list_page_recently_played">최근 재생한 앨범</string>
<string name="album_list_page_starred">즐겨찾기한 앨범</string>
<string name="album_list_page_title">앨범</string>
<string name="album_page_extra_info_button">유사항목 더 보기</string>
<string name="album_page_play_button">재생</string>
<string name="album_page_shuffle_button">셔플</string>
<string name="app_name">Tempo</string>
<string name="artist_adapter_radio_station_starting">탐색 중…</string>
<string name="artist_bottom_sheet_instant_mix">인스턴트 믹스</string>
<string name="artist_bottom_sheet_shuffle">셔플</string>
<string name="artist_catalogue_title">아티스트</string>
<string name="artist_catalogue_title_expanded">아티스트 찾아보기</string>
<string name="artist_error_retrieving_radio">아티스트의 라디오를 검색하는 중에 오류가 발생했습니다.</string>
<string name="artist_error_retrieving_tracks">아티스트의 트랙을 검색하는 중에 오류가 발생했습니다.</string>
<string name="artist_list_page_downloaded">다운로드한 아티스트</string>
<string name="artist_list_page_starred">즐겨찾기한 아티스트</string>
<string name="artist_list_page_title">아티스트</string>
<string name="artist_page_radio_button">라디오</string>
<string name="artist_page_shuffle_button">셔플</string>
<string name="artist_page_switch_layout_button">레이아웃 전환</string>
<string name="artist_page_title_album_more_like_this_button">유사한 항목 더 보기</string>
<string name="artist_page_title_album_section">앨범</string>
<string name="artist_page_title_biography_more_button">더 보기</string>
<string name="artist_page_title_biography_section">약력</string>
<string name="artist_page_title_most_streamed_song_section">가장 많이 스트리밍한 음악</string>
<string name="artist_page_title_most_streamed_song_see_all_button">모두 보기</string>
<string name="battery_optimization_negative_button">무시</string>
<string name="battery_optimization_neutral_button">다시 묻지 않기</string>
<string name="battery_optimization_positive_button">비활성</string>
<string name="connection_alert_dialog_negative_button">취소</string>
<string name="connection_alert_dialog_neutral_button">데이터 세이버 활성</string>
<string name="connection_alert_dialog_positive_button">OK</string>
<string name="connection_alert_dialog_summary">Wi-Fi가 연결되지 않은 상태에서 Subsonic 서버에 대한 액세스가 제한되었습니다. 이 경고를 다시 보지 않으려면 앱 설정에서 연결 확인을 비활성화 해주세요.</string>
<string name="connection_alert_dialog_title">Wi-Fi가 연결되지 않음</string>
<string name="delete_download_storage_dialog_negative_button">취소</string>
<string name="delete_download_storage_dialog_positive_button">계속</string>
<string name="delete_download_storage_dialog_summary">계속할 시 서버에서 다운로드한 모든 저장 항목이 영구적으로 삭제됩니다.</string>
<string name="delete_download_storage_dialog_title">저장된 항목 삭제</string>
<string name="description_empty_title">설명 란이 비어있습니다.</string>
<string name="download_directory_dialog_negative_button">취소</string>
<string name="download_directory_dialog_positive_button">다운로드</string>
<string name="download_directory_dialog_summary">하위 폴더를 제외한 해당 폴더의 모든 트랙이 다운로드됩니다.</string>
<string name="download_directory_dialog_title">트랙 다운로드</string>
<string name="download_info_empty_subtitle">음악을 다운로드하면 여기 표시됩니다.</string>
<string name="download_info_empty_title">다운로드 하지 않음</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s 항목</string>
<string name="download_item_single_subtitle_formatter">%1$s 항목</string>
<string name="download_storage_dialog_sub_summary">변경 사항을 저장하려면 앱을 다시 시작하세요.</string>
<string name="download_storage_dialog_summary">>다운로드한 파일을 다른 저장소로 변경하면 기존 저장소에서 다운로드한 파일은 즉시 삭제됩니다.</string>
<string name="download_storage_dialog_title">저장소 선택 옵션</string>
<string name="download_storage_external_dialog_positive_button">외부</string>
<string name="download_storage_internal_dialog_negative_button">내부</string>
<string name="download_title_section">다운로드</string>
<string name="downloaded_bottom_sheet_add_to_queue">재생목록에 추가</string>
<string name="downloaded_bottom_sheet_play_next">다음 재생</string>
<string name="downloaded_bottom_sheet_remove">제거</string>
<string name="downloaded_bottom_sheet_remove_all">모두 제거</string>
<string name="downloaded_bottom_sheet_shuffle">셔플</string>
<string name="empty_string" />
<string name="error_required">필수 항목</string>
<string name="error_server_prefix">http 또는 https 접두사가 필요합니다.</string>
<string name="exo_download_notification_channel_name">다운로드</string>
<string name="filter_info_selection">둘 이상의 필터를 선택해 주세요.</string>
<string name="filter_title">필터</string>
<string name="filter_title_expanded">장르 필터링</string>
<string name="genre_catalogue_title">장르 카탈로그</string>
<string name="genre_catalogue_title_expanded">장르 찾아보기</string>
<string name="home_subtitle_best_of">최애 아티스트의 인기곡</string>
<string name="home_subtitle_made_for_you">좋아하는 음악으로 믹스를 시작해 보세요.</string>
<string name="home_subtitle_new_internet_radio_station">새 라디오 추가</string>
<string name="home_subtitle_new_podcast_channel">새 팟캐스트 채널 추가</string>
<string name="home_sync_starred_cancel">취소</string>
<string name="home_sync_starred_download">다운로드</string>
<string name="home_sync_starred_subtitle">트랙 다운로드 시 많은 데이터 사용량이 발생할 수 있습니다.</string>
<string name="home_sync_starred_title">동기화가 필요한 즐겨찾기 트랙이 있는 것 같습니다.</string>
<string name="home_title_best_of">Best of</string>
<string name="home_title_discovery">Discovery</string>
<string name="home_title_discovery_shuffle_all_button">모두 셔플</string>
<string name="home_title_flashback">Flashback</string>
<string name="home_title_internet_radio_station">인터넷 라디오 스테이션</string>
<string name="home_title_last_played">최근 재생</string>
<string name="home_title_last_played_see_all_button">모두 보기</string>
<string name="home_title_last_week">지난 주</string>
<string name="home_title_made_for_you">Made for you</string>
<string name="home_title_most_played">가장 많이 재생</string>
<string name="home_title_most_played_see_all_button">모두 보기</string>
<string name="home_title_new_releases">New releases</string>
<string name="home_title_newest_podcasts">새로운 팟캐스트</string>
<string name="home_title_podcast_channels">채널</string>
<string name="home_title_podcast_channels_see_all_button">모두 보기</string>
<string name="home_title_radio_station">라디오 스테이션</string>
<string name="home_title_recently_added">최근 추가</string>
<string name="home_title_recently_added_see_all_button">모두 보기</string>
<string name="home_title_shares">공유</string>
<string name="home_title_starred_albums">★ 즐겨찾기한 앨범</string>
<string name="home_title_starred_albums_see_all_button">모두 보기</string>
<string name="home_title_starred_artists">★ 즐겨찾기한 아티스트</string>
<string name="home_title_starred_artists_see_all_button">모두 보기</string>
<string name="home_title_starred_tracks">★ 즐겨찾기한 트랙</string>
<string name="home_title_starred_tracks_see_all_button">모두 보기</string>
<string name="home_title_top_songs">자주 플레이한 음악</string>
<string name="label_dot_separator" translatable="false"></string>
<string name="label_placeholder" translatable="false">--</string>
<string name="library_title_album">앨범</string>
<string name="library_title_album_see_all_button">모두 보기</string>
<string name="library_title_artist">아티스트</string>
<string name="library_title_artist_see_all_button">모두 보기</string>
<string name="library_title_genre">장르</string>
<string name="library_title_genre_see_all_button">모두 보기</string>
<string name="library_title_music_folder">음악 폴더</string>
<string name="library_title_playlist">플레이리스트</string>
<string name="library_title_playlist_see_all_button">모두 보기</string>
<string name="login_empty">서버가 없습니다.</string>
<string name="login_title">Subsonic 서버</string>
<string name="login_title_expanded">Subsonic 서버</string>
<string name="media_route_menu_title">Cast</string>
<string name="menu_add_button">추가</string>
<string name="menu_download_all_button">모두 다운로드</string>
<string name="menu_download_label">다운로드</string>
<string name="menu_filter_all">모두</string>
<string name="menu_filter_download">다운로드한</string>
<string name="menu_group_by_album">앨범</string>
<string name="menu_group_by_artist">아티스트</string>
<string name="menu_group_by_genre">장르</string>
<string name="menu_group_by_track">트랙</string>
<string name="menu_group_by_year">년도</string>
<string name="menu_home_label">홈으로</string>
<string name="menu_library_label">라이브러리</string>
<string name="menu_search_button">검색</string>
<string name="menu_settings_button">셋팅</string>
<string name="menu_sort_artist">아티스트</string>
<string name="menu_sort_name">이름</string>
<string name="menu_sort_random">랜덤</string>
<string name="menu_sort_year">년도</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">재생목록 비우기</string>
<string name="player_server_priority">서버 우선순위</string>
<string name="playlist_catalogue_title">플레이리스트 카탈로그</string>
<string name="playlist_catalogue_title_expanded">플레이리스트 찾아보기</string>
<string name="playlist_chooser_dialog_empty">플레이리스트가 없습니다.</string>
<string name="playlist_chooser_dialog_negative_button">취소</string>
<string name="playlist_chooser_dialog_neutral_button">생성</string>
<string name="playlist_chooser_dialog_title">플레이리스트 추가</string>
<string name="playlist_counted_tracks">%1$d 트랙 • %2$s</string>
<string name="playlist_duration">재생시간 • %1$s</string>
<string name="playlist_editor_dialog_hint_name">플레이리스트 이름</string>
<string name="playlist_editor_dialog_negative_button">취소</string>
<string name="playlist_editor_dialog_neutral_button">삭제</string>
<string name="playlist_editor_dialog_positive_button">저장</string>
<string name="playlist_editor_dialog_title">플레이리스트 수정</string>
<string name="playlist_page_play_button">재생</string>
<string name="playlist_page_shuffle_button">셔플</string>
<string name="playlist_song_count">플레이리스트 • %1$d 곡</string>
<string name="podcast_bottom_sheet_add_to_queue">재생목록에 추가</string>
<string name="podcast_bottom_sheet_delete">제거</string>
<string name="podcast_bottom_sheet_download">다운로드</string>
<string name="podcast_bottom_sheet_go_to_channel">채널로 이동</string>
<string name="podcast_bottom_sheet_play_next">다음 재생</string>
<string name="podcast_bottom_sheet_remove">제거</string>
<string name="podcast_channel_catalogue_title">채널</string>
<string name="podcast_channel_catalogue_title_expanded">채널 찾아보기</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS Url</string>
<string name="podcast_channel_editor_dialog_title">팟캐스트 채널</string>
<string name="podcast_channel_page_title_description_section">설명</string>
<string name="podcast_channel_page_title_episode_section">에피소드</string>
<string name="podcast_channel_page_title_no_episode_available">가능한 에피소드가 없습니다.</string>
<string name="podcast_episode_download_request_snackbar">요청이 서버로 전송되었습니다.</string>
<string name="podcast_info_empty_button">섹션을 숨기려면 클릭하세요.\n다시 시작하면 적용됩니다.</string>
<string name="podcast_info_empty_subtitle">채널을 추가하면 표시됩니다.</string>
<string name="podcast_info_empty_title">팟캐스트를 찾을 수 없습니다.</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="radio_editor_dialog_hint_homepage_url">라디오 홈페이지 URL</string>
<string name="radio_editor_dialog_hint_name">라디오 이름</string>
<string name="radio_editor_dialog_hint_stream_url">라디오 스트리밍 URL</string>
<string name="radio_editor_dialog_negative_button">취소</string>
<string name="radio_editor_dialog_neutral_button">삭제</string>
<string name="radio_editor_dialog_positive_button">저장</string>
<string name="radio_editor_dialog_title">인터넷 라디오 섹션</string>
<string name="radio_station_info_empty_button">섹션을 숨기려면 클릭하세요.\n다시 시작하면 적용됩니다.</string>
<string name="radio_station_info_empty_subtitle">라디오 스테이션을 추가하면 표시됩니다.</string>
<string name="radio_station_info_empty_title">스테이션을 찾을 수 없습니다.</string>
<string name="rating_dialog_negative_button">취소</string>
<string name="rating_dialog_positive_button">저장</string>
<string name="rating_dialog_title">별점</string>
<string name="search_hint">제목, 아티스트, 앨범 검색</string>
<string name="search_info_minimum_characters">3자 이상 입력하세요.</string>
<string name="search_title_album">앨범</string>
<string name="search_title_artist">아티스트</string>
<string name="search_title_song">음악</string>
<string name="server_signup_dialog_action_low_security">낮은 보안</string>
<string name="server_signup_dialog_hint_name">서버 이름</string>
<string name="server_signup_dialog_hint_password">암호</string>
<string name="server_signup_dialog_hint_url">서버 URL</string>
<string name="server_signup_dialog_hint_username">사용자 이름</string>
<string name="server_signup_dialog_negative_button">취소</string>
<string name="server_signup_dialog_neutral_button">삭제</string>
<string name="server_signup_dialog_positive_button">저장</string>
<string name="server_signup_dialog_title">서버 추가</string>
<string name="server_unreachable_dialog_negative_button">취소</string>
<string name="server_unreachable_dialog_neutral_button">로그인으로 이동</string>
<string name="server_unreachable_dialog_positive_button">무시하고 계속</string>
<string name="server_unreachable_dialog_summary">요청한 서버를 사용할 수 없습니다. 계속하면 한 시간 동안 더 이상 경고하지 않습니다.</string>
<string name="server_unreachable_dialog_title">서버에 연결할 수 없음</string>
<string name="settings_about_summary">Tempo는 안드로이드용 경량 Subsonic 오픈 소스 음악 클라이언트입니다.</string>
<string name="settings_about_title">About</string>
<string name="settings_audio_transcode_download_format">트랜스코딩 포맷</string>
<string name="settings_audio_transcode_download_priority_summary">활성화 시, 아래 트랜스코딩 설정으로 트랙 강제 다운로드를 하지 않습니다.</string>
<string name="settings_audio_transcode_download_priority_title">다운로드 스트리밍에 사용할 서버 설정 우선순위 지정</string>
<string name="settings_audio_transcode_download_summary">활성화 시, 트랜스코딩된 트랙을 다운로드합니다.</string>
<string name="settings_audio_transcode_download_title">트랜스코딩 트랙 다운로드</string>
<string name="settings_audio_transcode_estimate_content_length_summary">활성화 시, 서버가 트랙의 예상 재생 시간을 묻습니다.</string>
<string name="settings_audio_transcode_estimate_content_length_title">예상 재생 시간</string>
<string name="settings_audio_transcode_format_download">다운로드용 트랜스코딩 포맷</string>
<string name="settings_audio_transcode_format_mobile">데이터 사용 시 트랜스코딩 포맷</string>
<string name="settings_audio_transcode_format_wifi">Wi-Fi 사용 시 트랜스코딩 포맷</string>
<string name="settings_audio_transcode_priority_summary">활성화 시, 아래 트랜스코딩 설정으로 트랙 강제 스트리밍을 하지 않습니다.</string>
<string name="settings_audio_transcode_priority_title">서버 트랜스코딩 우선순위 설정</string>
<string name="settings_audio_transcode_priority_toast">서버에 적용할 트랙 트랜스코딩 우선순위</string>
<string name="settings_buffering_strategy">버퍼링 전략</string>
<string name="settings_buffering_strategy_summary">변경 사항을 적용하려면 앱을 수동으로 다시 시작해야 합니다.</string>
<string name="settings_covers_cache">앨범 커버 캐시 크기</string>
<string name="settings_data_saving_mode_summary">데이터 소비를 줄이려면 앨범 커버 다운로드를 피하세요.</string>
<string name="settings_data_saving_mode_title">모바일 데이터 사용량 제한</string>
<string name="settings_delete_download_storage_summary">계속하면 저장된 모든 항목을 완전히 삭제합니다.</string>
<string name="settings_delete_download_storage_title">저장된 항목 삭제</string>
<string name="settings_download_storage_title">스토리지 다운로드</string>
<string name="settings_equalizer_summary">오디오 설정 적용</string>
<string name="settings_equalizer_title">이퀄라이저</string>
<string name="settings_github_link">https://github.com/CappielloAntonio/tempo</string>
<string name="settings_github_summary">Follow the development</string>
<string name="settings_github_title">Github</string>
<string name="settings_image_size">이미지 해상도 설정</string>
<string name="settings_language">언어</string>
<string name="settings_logout_title">로그아웃</string>
<string name="settings_max_bitrate_download">다운로드 비트 전송률</string>
<string name="settings_max_bitrate_mobile">데이터 사용 시 비트 전송률</string>
<string name="settings_max_bitrate_wifi">Wi-Fi 사용 시 비트 전송률</string>
<string name="settings_media_cache">미디어 파일 캐시 크기</string>
<string name="settings_music_directory">음악 디렉토리 보기</string>
<string name="settings_music_directory_summary">활성화 시, 음악 디렉터리 섹션을 표시합니다. 폴더 탐색이 제대로 작동하려면 서버가 이 기능을 지원해야 합니다.</string>
<string name="settings_podcast">팟캐스트 보기</string>
<string name="settings_podcast_summary">활성화 시, 팟캐스트 섹션을 표시합니다.</string>
<string name="settings_queue_syncing_countdown">동기화 타이머</string>
<string name="settings_queue_syncing_summary">활성화 시, 재생목록을 저장하여 재실행 시 상태를 불러올 수 있습니다.</string>
<string name="settings_queue_syncing_title">사용자의 재생목록 동기화</string>
<string name="settings_radio">라디오 보기</string>
<string name="settings_radio_summary">활성화 시, 라디오 섹션을 표시합니다.</string>
<string name="settings_replay_gain">replay gain 모드 설정</string>
<string name="settings_rounded_corner">모서리를 둥글게 하기</string>
<string name="settings_rounded_corner_size">모서리 크기</string>
<string name="settings_rounded_corner_size_summary">모서리의 곡률 각도를 설정합니다.</string>
<string name="settings_rounded_corner_summary">활성화 시, 렌더링된 모든 앨범 커버의 곡률 각도를 설정합니다. 다시 시작하면 적용됩니다.</string>
<string name="settings_scan_title">라이브러리 스캔</string>
<string name="settings_scrobble_title">음악 스크로블링 활성화</string>
<string name="settings_share_title">음악 공유 활성화</string>
<string name="settings_sub_summary_scrobble">스크로블링은 이 데이터를 수신할 수 있는 서버에 의존합니다.</string>
<string name="settings_summary_skip_min_star_rating">아티스트의 라디오를 들을 때, 인스턴트 믹스를 들을 때, 전체를 셔플할 때 특정 별점 이하의 트랙은 무시됩니다.</string>
<string name="settings_summary_replay_gain">Replay gain은 일관된 청취 경험을 위해 오디오 트랙의 볼륨 레벨을 조정할 수 있는 기능입니다. 이 설정은 필요한 메타데이터가 트랙에 포함된 경우에만 유효합니다.</string>
<string name="settings_summary_scrobble">스크로블링은 기기에서 들은 음악 정보를 음악 서버로 보내는 기능입니다. 이 정보는 음악 선호도에 따른 맞춤 추천을 생성하는 데 사용합니다.</string>
<string name="settings_summary_share">링크를 통해 음악을 공유할 수 있습니다. 이 기능은 서버 측에서 지원 및 활성화되어야 하며 개별 트랙, 앨범, 재생 목록으로 제한됩니다.</string>
<string name="settings_summary_syncing">사용자의 재생목록의 상태를 반환합니다. 재생목록의 트랙, 현재 재생 중인 트랙, 트랙 번호가 포함됩니다. 서버가 이 기능을 지원해야 합니다.</string>
<string name="settings_summary_transcoding">트랜스코딩 모드에 우선순위가 부여됩니다. \"직접 재생\"으로 설정하면 파일의 비트 전송률이 변경되지 않습니다.</string>
<string name="settings_summary_transcoding_download">트랜스코딩된 미디어를 다운로드합니다. 활성화하면 다운로드 endpoint를 사용하지 않고 다음 설정이 사용됩니다. \n\n \"다운로드용 트랜스코딩 포맷\"이 \"직접 다운로드\"로 설정된 경우 파일의 비트 전송률은 변경되지 않습니다.</string>
<string name="settings_summary_transcoding_estimate_content_length">파일이 즉시 트랜스코딩되면 일반적으로 트랙 길이를 표시하지 않습니다. 트랙의 재생시간을 추정하는 기능을 지원한다면 서버에 요청할 수 있지만 응답 시간이 필요할 수 있습니다.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">활성화 시, 즐겨찾기 트랙을 오프라인으로 사용할 수 있도록 다운로드합니다.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">오프라인 사용을 위해 즐겨찾기 트랙 동기화</string>
<string name="settings_theme">테마</string>
<string name="settings_title_data">데이터</string>
<string name="settings_title_general">일반</string>
<string name="settings_title_rating">별점</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_scrobble">스크로블링</string>
<string name="settings_title_skip_min_star_rating">별점 기준으로 트랙 무시</string>
<string name="settings_title_skip_min_star_rating_dialog">음악 별점:</string>
<string name="settings_title_share">공유</string>
<string name="settings_title_syncing">동기화</string>
<string name="settings_title_transcoding">트랜스코딩</string>
<string name="settings_title_transcoding_download">트랜스코딩 다운로드</string>
<string name="settings_title_ui">UI</string>
<string name="settings_transcoded_download">트랜스코딩된 다운로드</string>
<string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="settings_version_title">버전</string>
<string name="settings_wifi_only_summary">모바일 데이터로 스트리밍하려 할 시 확인창을 띄웁니다.</string>
<string name="settings_wifi_only_title">Wi-Fi로만 스트리밍 확인창</string>
<string name="share_bottom_sheet_copy_link">링크 복사</string>
<string name="share_bottom_sheet_delete">공유 삭제</string>
<string name="share_bottom_sheet_update">공유 업데이트</string>
<string name="share_subtitle_item">만료일: %1$s</string>
<string name="share_unsupported_error">공유 기능을 지원하지 않거나 활성화되지 않았습니다.</string>
<string name="share_update_dialog_hint_description">설명</string>
<string name="share_update_dialog_hint_expiration_date">만료일</string>
<string name="share_update_dialog_negative_button">취소</string>
<string name="share_update_dialog_positive_button">저장</string>
<string name="share_update_dialog_title">공유</string>
<string name="song_bottom_sheet_add_to_playlist">플레이리스트에 추가</string>
<string name="song_bottom_sheet_add_to_queue">재생목록에 추가</string>
<string name="song_bottom_sheet_download">다운로드</string>
<string name="song_bottom_sheet_error_retrieving_album">앨범 검색 중 오류가 발생했습니다.</string>
<string name="song_bottom_sheet_error_retrieving_artist">아티스트 검색 중 오류가 발생했습니다.</string>
<string name="song_bottom_sheet_go_to_album">앨범으로 이동</string>
<string name="song_bottom_sheet_go_to_artist">아티스트로 이동</string>
<string name="song_bottom_sheet_instant_mix">인스턴트 믹스</string>
<string name="song_bottom_sheet_play_next">다음 재생</string>
<string name="song_bottom_sheet_rate">별점</string>
<string name="song_bottom_sheet_remove">제거</string>
<string name="song_bottom_sheet_share">공유</string>
<string name="song_list_page_downloaded">다운로드됨</string>
<string name="song_list_page_most_played">가장 많이 재생한 트랙</string>
<string name="song_list_page_recently_added">최근 추가한 트랙</string>
<string name="song_list_page_recently_played">최근 재생한 트랙</string>
<string name="song_list_page_starred">즐겨찾기한 트랙</string>
<string name="song_list_page_top">%1$s\의 top tracks</string>
<string name="song_list_page_year">년도 %1$d</string>
<string name="song_subtitle_formatter">%1$s • %2$s</string>
<string name="starred_sync_dialog_negative_button">취소</string>
<string name="starred_sync_dialog_neutral_button">계속</string>
<string name="starred_sync_dialog_positive_button">계속해서 다운로드</string>
<string name="starred_sync_dialog_summary">즐겨찾기한 트랙을 다운로드할 시 많은 양의 데이터가 필요할 수 있습니다.</string>
<string name="starred_sync_dialog_title">즐겨찾기 한 트랙 동기화</string>
<string name="track_info_album">앨범</string>
<string name="track_info_artist">아티스트</string>
<string name="track_info_bitrate">비트 전송률</string>
<string name="track_info_content_type">컨텐츠 타입</string>
<string name="track_info_dialog_positive_button">OK</string>
<string name="track_info_dialog_title">트랙 정보</string>
<string name="track_info_disc_number">디스크 번호</string>
<string name="track_info_duration">재생 시간</string>
<string name="track_info_genre">장르</string>
<string name="track_info_path">경로</string>
<string name="track_info_size">크기</string>
<string name="track_info_suffix">접미사</string>
<string name="track_info_summary_downloaded_file">파일은 Subsonic API를 사용하여 다운로드되었습니다. 파일의 코덱과 비트 전송률은 소스 파일과 동일하게 유지됩니다.</string>
<string name="track_info_summary_full_transcode">서버에 파일을 트랜스코딩하고 비트 전송률을 수정하도록 요청합니다. 사용자가 요청한 코덱은 %1$s이고 비트 전송률은 %2$s입니다. 선택한 형식의 파일 코덱 및 비트 전송률에 대한 변경 사항은 서버에서 수행하며 서버가 이를 지원하지 않을 수 있습니다.</string>
<string name="track_info_summary_original_file">서버에서 제공한 원본 파일만 읽습니다. 앱은 원본 소스의 비트 전송률을 사용하여 트랜스코딩되지 않은 파일을 서버에 명시적으로 요청합니다.</string>
<string name="track_info_summary_server_prioritized">파일의 품질을 서버 설정에 맞춥니다. 앱은 잠재적인 트랜스코딩에 대해 코덱 및 비트 전송률 선택을 강제하지 않습니다.</string>
<string name="track_info_summary_transcoding_bitrate">서버에 파일의 비트 전송률을 수정하도록 요청합니다. 사용자가 %1$s의 비트 전송률을 요청해도 소스 파일의 코덱은 동일하게 유지됩니다. 선택한 형식의 파일 비트 전송률에 대한 변경 사항은 서버에서 수행하며 서버가 이를 지원하지 않을 수 있습니다.</string>
<string name="track_info_summary_transcoding_codec">서버에 파일을 트랜스코딩 하도록 요청합니다. 사용자가 요청한 코덱은 %1$s이고 비트 전송률은 소스 파일과 동일합니다. 서버가 기능을 지원할 시에만 선택한 형식으로 트랜스코딩 됩니다.</string>
<string name="track_info_title">타이틀</string>
<string name="track_info_track_number">트랙 번호</string>
<string name="track_info_transcoded_content_type">트랜스코딩된 콘텐츠 유형</string>
<string name="track_info_transcoded_suffix">트랜스코딩된 접미사</string>
<string name="track_info_year">년도</string>
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">이 앱을 일러스트로 더 다채롭게 꾸밀 수 있도록 해준 unDraw 에 특별히 감사드립니다.</string>
<string name="undraw_url">https://undraw.co/</string>
</resources>

View File

@@ -0,0 +1,227 @@
<resources>
<string-array name="theme_list_titles">
<item>浅色</item>
<item>深色</item>
<item>跟随系统</item>
</string-array>
<string-array name="theme_list_values">
<item>light</item>
<item>dark</item>
<item>default</item>
</string-array>
<string-array name="pref_cache_size_titles">
<item></item>
<item></item>
<item></item>
</string-array>
<string-array name="pref_cache_size_values">
<item>500</item>
<item>250</item>
<item>125</item>
</string-array>
<string-array name="pref_image_size_titles">
<item></item>
<item></item>
<item></item>
</string-array>
<string-array name="pref_image_size_values">
<item>-1</item>
<item>500</item>
<item>300</item>
</string-array>
<string-array name="max_bitrate_wifi_list_titles">
<item>原始</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_wifi_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="max_bitrate_mobile_list_titles">
<item>原始</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_mobile_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="max_bitrate_download_list_titles">
<item>原始</item>
<item>32 kbps</item>
<item>48 kbps</item>
<item>64 kbps</item>
<item>80 kbps</item>
<item>96 kbps</item>
<item>112 kbps</item>
<item>128 kbps</item>
<item>160 kbps</item>
<item>192 kbps</item>
<item>256 kbps</item>
<item>320 kbps</item>
</string-array>
<string-array name="max_bitrate_download_list_values">
<item>0</item>
<item>32</item>
<item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
<item>112</item>
<item>128</item>
<item>160</item>
<item>192</item>
<item>256</item>
<item>320</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_titles">
<item>播放原始</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_wifi_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_titles">
<item>播放原始</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_mobile_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_titles">
<item>下载原始</item>
<item>Opus</item>
<item>AAC</item>
<item>Mp3</item>
<item>Flac</item>
</string-array>
<string-array name="audio_transcode_format_download_list_values">
<item>raw</item>
<item>opus</item>
<item>aac</item>
<item>mp3</item>
<item>flac</item>
</string-array>
<string-array name="queue_syncing_countdown_titles">
<item>10秒</item>
<item>5秒</item>
<item>2秒</item>
</string-array>
<string-array name="queue_syncing_countdown_values">
<item>10</item>
<item>5</item>
<item>2</item>
</string-array>
<string-array name="rounded_corner_size_titles">
<item></item>
<item></item>
<item></item>
</string-array>
<string-array name="rounded_corner_size_values">
<item>18</item>
<item>12</item>
<item>6</item>
</string-array>
<string-array name="replay_gain_titles">
<item>禁用</item>
<item>曲目</item>
<item>专辑</item>
<item>自动</item>
</string-array>
<string-array name="replay_gain_values">
<item>disabled</item>
<item>track</item>
<item>album</item>
<item>auto</item>
</string-array>
<string-array name="transcoded_download_option_list_titles">
<item>不转码</item>
<item>服务器设置</item>
<item>Wi-Fi转码设置</item>
<item>移动数据转码设置</item>
</string-array>
<string-array name="transcoded_download_option_list_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="buffering_strategy_titles">
<item>最小</item>
<item>适中</item>
<item>积极</item>
<item>极端</item>
</string-array>
<string-array name="buffering_strategy_values">
<item>.1</item>
<item>1</item>
<item>4</item>
<item>8</item>
</string-array>
</resources>

View File

@@ -0,0 +1,362 @@
<resources>
<string name="activity_battery_optimizations_conclusion">如果遇到问题,请访问 https://dontkillmyapp.com。 省电优化选项可能会影响应用的性能,网站上提供了如何禁用这些选项的详细说明。</string>
<string name="activity_battery_optimizations_summary">请禁用针对媒体锁屏播放的电池优化。</string>
<string name="activity_battery_optimizations_title">电池优化</string>
<string name="activity_info_offline_mode">离线模式</string>
<string name="album_bottom_sheet_add_to_queue">添加到队列</string>
<string name="album_bottom_sheet_download_all">全部下载</string>
<string name="album_bottom_sheet_go_to_artist">查看该艺术家</string>
<string name="album_bottom_sheet_instant_mix">即时混合</string>
<string name="album_bottom_sheet_play_next">下一首播放</string>
<string name="album_bottom_sheet_remove_all">移除所有</string>
<string name="album_bottom_sheet_share">分享</string>
<string name="album_bottom_sheet_shuffle">随机播放</string>
<string name="album_catalogue_title">专辑</string>
<string name="album_catalogue_title_expanded">浏览专辑</string>
<string name="album_error_retrieving_artist">检索艺术家时出错</string>
<string name="album_list_page_downloaded">已下载的专辑</string>
<string name="album_list_page_most_played">最常播放的专辑</string>
<string name="album_list_page_new_releases">新发行</string>
<string name="album_list_page_recently_added">最近添加的专辑</string>
<string name="album_list_page_recently_played">最近播放的专辑</string>
<string name="album_list_page_starred">收藏的专辑</string>
<string name="album_list_page_title">专辑</string>
<string name="album_page_extra_info_button">更多相似</string>
<string name="album_page_play_button">播放</string>
<string name="album_page_shuffle_button">随机播放</string>
<string name="app_name">Tempo</string>
<string name="artist_adapter_radio_station_starting">正在搜索...</string>
<string name="artist_bottom_sheet_instant_mix">即时混合</string>
<string name="artist_bottom_sheet_shuffle">随机播放</string>
<string name="artist_catalogue_title">艺术家</string>
<string name="artist_catalogue_title_expanded">浏览艺术家</string>
<string name="artist_error_retrieving_radio">检索艺术家的电台时出错</string>
<string name="artist_error_retrieving_tracks">检索艺术家曲目时出错</string>
<string name="artist_list_page_downloaded">已下载的艺术家</string>
<string name="artist_list_page_starred">收藏的艺人</string>
<string name="artist_list_page_title">艺术家</string>
<string name="artist_page_radio_button">电台</string>
<string name="artist_page_shuffle_button">随机播放</string>
<string name="artist_page_switch_layout_button">切换布局</string>
<string name="artist_page_title_album_more_like_this_button">更多相似</string>
<string name="artist_page_title_album_section">专辑</string>
<string name="artist_page_title_biography_more_button">更多</string>
<string name="artist_page_title_biography_section">个人简介</string>
<string name="artist_page_title_most_streamed_song_section">最常播放的歌曲</string>
<string name="artist_page_title_most_streamed_song_see_all_button">查看全部</string>
<string name="battery_optimization_negative_button">忽略</string>
<string name="battery_optimization_neutral_button">不要再问</string>
<string name="battery_optimization_positive_button">禁用</string>
<string name="connection_alert_dialog_negative_button">取消</string>
<string name="connection_alert_dialog_neutral_button">启用流量节省</string>
<string name="connection_alert_dialog_positive_button">确定</string>
<string name="connection_alert_dialog_summary">已限制通过 Wi-Fi 以外的连接访问 Subsonic 服务器。 要阻止此警告对话框再次出现,请在应用程序设置中禁用连接检查。</string>
<string name="connection_alert_dialog_title">Wi-Fi网络未连接</string>
<string name="delete_download_storage_dialog_negative_button">取消</string>
<string name="delete_download_storage_dialog_positive_button">继续</string>
<string name="delete_download_storage_dialog_summary">请注意,继续执行此操作将永久删除从所有服务器下载的所有已保存的项目。</string>
<string name="delete_download_storage_dialog_title">删除已保存的项目</string>
<string name="description_empty_title">没有可用的描述</string>
<string name="download_directory_dialog_negative_button">取消</string>
<string name="download_directory_dialog_positive_button">下载</string>
<string name="download_directory_dialog_summary">该文件夹中的所有曲目将被下载。 子文件夹中的曲目将不会被下载。</string>
<string name="download_directory_dialog_title">下载曲目</string>
<string name="download_info_empty_subtitle">下载歌曲后,您可以在这里找到它。</string>
<string name="download_info_empty_title">还没有下载!</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
<string name="download_item_single_subtitle_formatter">%1$s items</string>
<string name="download_storage_dialog_sub_summary">要使更改生效,请重新启动应用程序。</string>
<string name="download_storage_dialog_summary">更改已下载文件的目录将会立即删除以前已下载的所有文件。</string>
<string name="download_storage_dialog_title">选择存储选项</string>
<string name="download_storage_external_dialog_positive_button">外部</string>
<string name="download_storage_internal_dialog_negative_button">内部</string>
<string name="download_title_section">下载</string>
<string name="downloaded_bottom_sheet_add_to_queue">添加到队列</string>
<string name="downloaded_bottom_sheet_play_next">下一首播放</string>
<string name="downloaded_bottom_sheet_remove">移除</string>
<string name="downloaded_bottom_sheet_remove_all">移除所有</string>
<string name="downloaded_bottom_sheet_shuffle">随机播放</string>
<string name="empty_string"></string>
<string name="error_required">必需</string>
<string name="error_server_prefix">必须是 http 或 https 前缀</string>
<string name="exo_download_notification_channel_name">下载</string>
<string name="filter_info_selection">选择两个或多个过滤器</string>
<string name="filter_title">筛选</string>
<string name="filter_title_expanded">筛选流派</string>
<string name="genre_catalogue_title">流派目录</string>
<string name="genre_catalogue_title_expanded">浏览流派</string>
<string name="home_subtitle_best_of">您最喜欢的艺术家的热门歌曲</string>
<string name="home_subtitle_made_for_you">从您喜欢的歌曲开始混音</string>
<string name="home_subtitle_new_internet_radio_station">添加新的电台</string>
<string name="home_subtitle_new_podcast_channel">添加新的播客频道</string>
<string name="home_sync_starred_cancel">取消</string>
<string name="home_sync_starred_download">下载</string>
<string name="home_sync_starred_subtitle">下载这些曲目可能需要大量移动数据</string>
<string name="home_sync_starred_title">似乎有一些已收藏的曲目需要同步</string>
<string name="home_title_best_of">最佳</string>
<string name="home_title_discovery">发现</string>
<string name="home_title_discovery_shuffle_all_button">全部随机播放</string>
<string name="home_title_flashback">闪回</string>
<string name="home_title_internet_radio_station">网络广播电台</string>
<string name="home_title_last_played">最近播放</string>
<string name="home_title_last_played_see_all_button">查看全部</string>
<string name="home_title_last_week">上周</string>
<string name="home_title_made_for_you">为您定制</string>
<string name="home_title_most_played">最常播放</string>
<string name="home_title_most_played_see_all_button">查看全部</string>
<string name="home_title_new_releases">新发行</string>
<string name="home_title_newest_podcasts">最新播客</string>
<string name="home_title_podcast_channels">频道</string>
<string name="home_title_podcast_channels_see_all_button">查看全部</string>
<string name="home_title_radio_station">广播电台</string>
<string name="home_title_recently_added">最近添加</string>
<string name="home_title_recently_added_see_all_button">查看全部</string>
<string name="home_title_shares">分享</string>
<string name="home_title_starred_albums">★ 收藏的专辑</string>
<string name="home_title_starred_albums_see_all_button">查看全部</string>
<string name="home_title_starred_artists">★ 收藏的艺术家</string>
<string name="home_title_starred_artists_see_all_button">查看全部</string>
<string name="home_title_starred_tracks">★ 收藏的曲目</string>
<string name="home_title_starred_tracks_see_all_button">查看全部</string>
<string name="home_title_top_songs">你最喜欢的歌曲</string>
<string name="label_dot_separator" translatable="false"></string>
<string name="label_placeholder" translatable="false">--</string>
<string name="library_title_album">专辑</string>
<string name="library_title_album_see_all_button">查看全部</string>
<string name="library_title_artist">艺术家</string>
<string name="library_title_artist_see_all_button">查看全部</string>
<string name="library_title_genre">流派</string>
<string name="library_title_genre_see_all_button">查看全部</string>
<string name="library_title_music_folder">音乐文件夹</string>
<string name="library_title_playlist">播放列表</string>
<string name="library_title_playlist_see_all_button">查看全部</string>
<string name="login_empty">尚未添加服务器</string>
<string name="login_title">Subsonic 服务器</string>
<string name="login_title_expanded">Subsonic 服务器</string>
<string name="media_route_menu_title">投送</string>
<string name="menu_add_button">添加</string>
<string name="menu_download_all_button">全部下载</string>
<string name="menu_download_label">下载</string>
<string name="menu_filter_all">全部</string>
<string name="menu_filter_download">已下载</string>
<string name="menu_group_by_album">专辑</string>
<string name="menu_group_by_artist">艺术家</string>
<string name="menu_group_by_genre">流派</string>
<string name="menu_group_by_track">曲目</string>
<string name="menu_group_by_year">年份</string>
<string name="menu_home_label">首页</string>
<string name="menu_library_label">曲库</string>
<string name="menu_search_button">搜索</string>
<string name="menu_settings_button">设置</string>
<string name="menu_sort_artist">艺术家</string>
<string name="menu_sort_name">姓名</string>
<string name="menu_sort_random">随机</string>
<string name="menu_sort_year">年份</string>
<string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">清空队列</string>
<string name="player_server_priority">服务器优先级</string>
<string name="playlist_catalogue_title">播放列表目录</string>
<string name="playlist_catalogue_title_expanded">浏览播放列表</string>
<string name="playlist_chooser_dialog_empty">尚未创建播放列表</string>
<string name="playlist_chooser_dialog_negative_button">取消</string>
<string name="playlist_chooser_dialog_neutral_button">新建</string>
<string name="playlist_chooser_dialog_title">添加到播放列表</string>
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
<string name="playlist_duration">持续时间 • %1$s</string>
<string name="playlist_editor_dialog_hint_name">播放列表名称</string>
<string name="playlist_editor_dialog_negative_button">取消</string>
<string name="playlist_editor_dialog_neutral_button">删除</string>
<string name="playlist_editor_dialog_positive_button">保存</string>
<string name="playlist_editor_dialog_title">编辑播放列表</string>
<string name="playlist_page_play_button">播放</string>
<string name="playlist_page_shuffle_button">随机播放</string>
<string name="playlist_song_count">播放列表 • %1$d songs</string>
<string name="podcast_bottom_sheet_add_to_queue">添加到队列</string>
<string name="podcast_bottom_sheet_delete">删除</string>
<string name="podcast_bottom_sheet_download">下载</string>
<string name="podcast_bottom_sheet_go_to_channel">前往该频道</string>
<string name="podcast_bottom_sheet_play_next">下一首播放</string>
<string name="podcast_bottom_sheet_remove">移除</string>
<string name="podcast_channel_catalogue_title">频道</string>
<string name="podcast_channel_catalogue_title_expanded">浏览频道</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">RSS 网址</string>
<string name="podcast_channel_editor_dialog_title">播客频道</string>
<string name="podcast_channel_page_title_description_section">描述</string>
<string name="podcast_channel_page_title_episode_section">剧集</string>
<string name="podcast_channel_page_title_no_episode_available">没有可用的剧集</string>
<string name="podcast_episode_download_request_snackbar">您的请求已发送至服务器</string>
<string name="podcast_info_empty_button">单击以隐藏该部分\n重启应用后生效</string>
<string name="podcast_info_empty_subtitle">添加频道后,您将在此处找到它</string>
<string name="podcast_info_empty_title">未找到播客!</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="radio_editor_dialog_hint_homepage_url">电台主页 URL</string>
<string name="radio_editor_dialog_hint_name">电台名称</string>
<string name="radio_editor_dialog_hint_stream_url">广播流 URL</string>
<string name="radio_editor_dialog_negative_button">取消</string>
<string name="radio_editor_dialog_neutral_button">删除</string>
<string name="radio_editor_dialog_positive_button">保存</string>
<string name="radio_editor_dialog_title">网络广播电台</string>
<string name="radio_station_info_empty_button">单击以隐藏该部分\n重启应用后生效</string>
<string name="radio_station_info_empty_subtitle">添加广播电台后,您可以在此处找到它</string>
<string name="radio_station_info_empty_title">没有找到电台!</string>
<string name="rating_dialog_negative_button">取消</string>
<string name="rating_dialog_positive_button">保存</string>
<string name="rating_dialog_title">评分</string>
<string name="search_hint">搜索标题、艺术家或专辑</string>
<string name="search_info_minimum_characters">输入至少三个字符</string>
<string name="search_title_album">专辑</string>
<string name="search_title_artist">艺术家</string>
<string name="search_title_song">歌曲</string>
<string name="server_signup_dialog_action_low_security">低安全性</string>
<string name="server_signup_dialog_hint_name">服务器名称</string>
<string name="server_signup_dialog_hint_password">密码</string>
<string name="server_signup_dialog_hint_url">服务器地址</string>
<string name="server_signup_dialog_hint_username">用户名</string>
<string name="server_signup_dialog_negative_button">取消</string>
<string name="server_signup_dialog_neutral_button">删除</string>
<string name="server_signup_dialog_positive_button">保存</string>
<string name="server_signup_dialog_title">添加服务器</string>
<string name="server_unreachable_dialog_negative_button">取消</string>
<string name="server_unreachable_dialog_neutral_button">前往登录</string>
<string name="server_unreachable_dialog_positive_button">仍然继续</string>
<string name="server_unreachable_dialog_summary">请求的服务器不可用。 如果您选择继续,此对话框在接下来的一个小时内将不会再次出现。</string>
<string name="server_unreachable_dialog_title">服务器无法访问</string>
<string name="settings_about_summary">Tempo 是 Subsonic 的开源轻量级音乐客户端,专为 Android 设计和构建。</string>
<string name="settings_about_title">关于</string>
<string name="settings_audio_transcode_download_format">转码格式</string>
<string name="settings_audio_transcode_download_priority_summary">如果启用Tempo 将不会强制使用下面的转码设置下载曲目。</string>
<string name="settings_audio_transcode_download_priority_title">优先考虑服务器上用于流式传输的设置</string>
<string name="settings_audio_transcode_download_summary">如果启用Tempo 将下载转码后的曲目。</string>
<string name="settings_audio_transcode_download_title">下载转码后的曲目</string>
<string name="settings_audio_transcode_estimate_content_length_summary">如果启用,将发送请求到服务器以查询曲目的估计持续时间。</string>
<string name="settings_audio_transcode_estimate_content_length_title">估计内容长度</string>
<string name="settings_audio_transcode_format_download">用于下载的转码格式</string>
<string name="settings_audio_transcode_format_mobile">移动数据下的转码格式</string>
<string name="settings_audio_transcode_format_wifi">Wi-Fi 下的转码格式</string>
<string name="settings_audio_transcode_priority_summary">如果启用Tempo 将不会强制使用下面的转码设置流式传输曲目。</string>
<string name="settings_audio_transcode_priority_title">优先考虑服务器转码设置</string>
<string name="settings_audio_transcode_priority_toast">曲目转码设置优先级设置为服务器</string>
<string name="settings_buffering_strategy">缓存策略</string>
<string name="settings_buffering_strategy_summary">为了使更改生效,您必须手动重新启动应用程序。</string>
<string name="settings_covers_cache">图片缓存大小</string>
<string name="settings_data_saving_mode_summary">为了减少数据消耗,请避免下载封面。</string>
<string name="settings_data_saving_mode_title">限制移动数据使用</string>
<string name="settings_delete_download_storage_summary">继续当前操作将导致所有已保存的项目被永久删除。</string>
<string name="settings_delete_download_storage_title">删除已保存的项目</string>
<string name="settings_download_storage_title">下载存储</string>
<string name="settings_equalizer_summary">调整音频设置</string>
<string name="settings_equalizer_title">均衡器</string>
<string name="settings_github_link">https://github.com/CappielloAntonio/tempo</string>
<string name="settings_github_summary">关注开发进展</string>
<string name="settings_github_title">Github</string>
<string name="settings_image_size">设置图像分辨率</string>
<string name="settings_language">语言</string>
<string name="settings_logout_title">注销登录</string>
<string name="settings_max_bitrate_download">用于下载的比特率</string>
<string name="settings_max_bitrate_mobile">移动数据下的比特率</string>
<string name="settings_max_bitrate_wifi">Wi-Fi 下的比特率</string>
<string name="settings_media_cache">媒体文件缓存大小</string>
<string name="settings_music_directory">显示音乐目录</string>
<string name="settings_music_directory_summary">如果启用,则显示音乐目录部分。 请注意,要使文件夹导航正常工作,服务器必须支持此功能。</string>
<string name="settings_podcast">显示播客</string>
<string name="settings_podcast_summary">如果启用,则显示播客部分。</string>
<string name="settings_queue_syncing_countdown">同步定时器</string>
<string name="settings_queue_syncing_summary">如果启用,将允许当前用户保存其播放队列,并能够在打开应用程序时加载保存状态。</string>
<string name="settings_queue_syncing_title">同步当前用户的播放队列</string>
<string name="settings_radio">显示广播</string>
<string name="settings_radio_summary">如果启用,则显示电台部分。</string>
<string name="settings_replay_gain">设置播放增益模式</string>
<string name="settings_rounded_corner">圆角</string>
<string name="settings_rounded_corner_size">圆角大小</string>
<string name="settings_rounded_corner_size_summary">设置圆角的大小。</string>
<string name="settings_rounded_corner_summary">如果启用,则为所有渲染的封面设置圆角。 更改将在应用重新启动后生效。</string>
<string name="settings_scan_title">扫描曲库</string>
<string name="settings_scrobble_title">启用音乐记录</string>
<string name="settings_share_title">启用音乐共享</string>
<string name="settings_sub_summary_scrobble">请注意,音乐记录同时也依赖于服务器是否能够接收这些数据。</string>
<string name="settings_summary_replay_gain">播放增益Replay gain允许您通过调整音轨的音量以获得始终如一的聆听体验。 仅当曲目标签包含必要的元数据时,此设置才有效。</string>
<string name="settings_summary_scrobble">音乐记录Scrobbling允许您的设备将您收听的歌曲的相关信息发送到音乐服务器。 这些信息有助于基于您的音乐偏好生成个性化推荐。</string>
<string name="settings_summary_share">允许用户通过链接共享音乐。 该功能需要服务器端支持并启用,并且仅限于单个曲目、专辑和队列。</string>
<string name="settings_summary_syncing">返回当前用户的播放队列状态。 这包括播放队列中的曲目、正在播放的曲目以及曲目播放进度。需要服务器支持此功能。</string>
<string name="settings_summary_transcoding">转码模式优先级设置。 如果设置为“播放原始”,文件的比特率将不会更改。</string>
<string name="settings_summary_transcoding_download">下载转码后的媒体。 如果启用,将不会下载原始数据,而是使用以下设置。\n如果“用于下载的转码格式”设置为“下载原始”则文件的比特率不会更改。</string>
<string name="settings_summary_transcoding_estimate_content_length">当文件即时转码时,客户端通常不会显示曲目长度。 可以向支持该功能的服务器发送请求,估计正在播放的曲目的持续时间,但可能响应变慢。</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">如果启用,将下载已收藏的曲目以供离线使用。</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">同步已收藏的曲目以供离线使用</string>
<string name="settings_theme">主题</string>
<string name="settings_title_data">数据</string>
<string name="settings_title_general">通用</string>
<string name="settings_title_replay_gain">播放增益</string>
<string name="settings_title_scrobble">音乐记录</string>
<string name="settings_title_share">分享</string>
<string name="settings_title_syncing">同步</string>
<string name="settings_title_transcoding">转码</string>
<string name="settings_title_transcoding_download">转码下载</string>
<string name="settings_title_ui">界面</string>
<string name="settings_transcoded_download">转码下载</string>
<string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="settings_version_title">版本</string>
<string name="settings_wifi_only_summary">在通过移动网络进行流式传输之前请求用户确认。</string>
<string name="settings_wifi_only_title">提示仅通过 Wi-Fi 进行流式传输</string>
<string name="share_bottom_sheet_copy_link">复制链接</string>
<string name="share_bottom_sheet_delete">删除分享</string>
<string name="share_bottom_sheet_update">更新分享</string>
<string name="share_subtitle_item">到期日期:%1$s</string>
<string name="share_unsupported_error">不支持分享或未启用</string>
<string name="share_update_dialog_hint_description">描述</string>
<string name="share_update_dialog_hint_expiration_date">截止日期</string>
<string name="share_update_dialog_negative_button">取消</string>
<string name="share_update_dialog_positive_button">保存</string>
<string name="share_update_dialog_title">分享</string>
<string name="song_bottom_sheet_add_to_playlist">添加到播放列表</string>
<string name="song_bottom_sheet_add_to_queue">添加到队列</string>
<string name="song_bottom_sheet_download">下载</string>
<string name="song_bottom_sheet_error_retrieving_album">检索相册时出错</string>
<string name="song_bottom_sheet_error_retrieving_artist">检索艺术家时出错</string>
<string name="song_bottom_sheet_go_to_album">前往该专辑</string>
<string name="song_bottom_sheet_go_to_artist">前往该艺术家</string>
<string name="song_bottom_sheet_instant_mix">即时混合</string>
<string name="song_bottom_sheet_play_next">下一首播放</string>
<string name="song_bottom_sheet_rate">评分</string>
<string name="song_bottom_sheet_remove">移除</string>
<string name="song_bottom_sheet_share">分享</string>
<string name="song_list_page_downloaded">已下载</string>
<string name="song_list_page_most_played">最常播放的曲目</string>
<string name="song_list_page_recently_added">最近添加的曲目</string>
<string name="song_list_page_recently_played">最近播放的曲目</string>
<string name="song_list_page_starred">已收藏的曲目</string>
<string name="song_list_page_top">%1$s 的热门曲目</string>
<string name="song_list_page_year">年份 %1$d</string>
<string name="song_subtitle_formatter">%1$s • %2$s</string>
<string name="starred_sync_dialog_negative_button">取消</string>
<string name="starred_sync_dialog_neutral_button">继续</string>
<string name="starred_sync_dialog_positive_button">继续并下载</string>
<string name="starred_sync_dialog_summary">下载收藏曲目可能需要大量数据。</string>
<string name="starred_sync_dialog_title">同步已收藏的曲目</string>
<string name="track_info_album">专辑</string>
<string name="track_info_artist">艺术家</string>
<string name="track_info_bitrate">比特率</string>
<string name="track_info_content_type">内容类型</string>
<string name="track_info_dialog_positive_button">确定</string>
<string name="track_info_dialog_title">曲目信息</string>
<string name="track_info_disc_number">碟片编号</string>
<string name="track_info_duration">持续时间</string>
<string name="track_info_genre">流派</string>
<string name="track_info_path">路径</string>
<string name="track_info_size">大小</string>
<string name="track_info_suffix">后缀</string>
<string name="track_info_summary_downloaded_file">该文件已使用 Subsonic API 下载。 文件的编码和比特率与源文件一致。</string>
<string name="track_info_summary_full_transcode">本应用将请求服务器对文件进行转码并修改其比特率。 用户请求的编解码器是%1$s比特率为%2$s。 对所选格式的文件的编码和比特率的任何潜在更改都将由服务器处理,服务器可能支持也可能不支持该操作。</string>
<string name="track_info_summary_original_file">本应用只会读取服务器提供的原始文件。 本应用将明确向服务器请求具有原始源比特率的未转码文件。</string>
<string name="track_info_summary_server_prioritized">要播放的文件质量取决于服务器设置。 本应用不会强制选择任何用于潜在转码的编码和比特率。</string>
<string name="track_info_summary_transcoding_bitrate">本应用将请求服务器修改文件的比特率。 用户请求的比特率为%1$s而源文件的编码将保持不变。 对所选格式的文件比特率的任何更改都将由服务器完成,服务器可能支持也可能不支持该操作。</string>
<string name="track_info_summary_transcoding_codec">本应用将请求服务器对文件进行转码。 用户请求的编解码器是%1$s而比特率将与源文件相同。 将文件转码为所选格式的可能性取决于服务器,因为它可能支持也可能不支持该操作。</string>
<string name="track_info_title">标题</string>
<string name="track_info_track_number">曲目编号</string>
<string name="track_info_transcoded_content_type">转码内容类型</string>
<string name="track_info_transcoded_suffix">转码后缀</string>
<string name="track_info_year">年份</string>
<string name="undraw_page">unDraw</string>
<string name="undraw_thanks">特别感谢 unDraw没有它提供的插图我们的应用不可能会如此精美。</string>
<string name="undraw_url">https://undraw.co/</string>
</resources>

View File

@@ -224,4 +224,19 @@
<item>4</item>
<item>8</item>
</string-array>
<string-array name="skip_min_star_rating_titles">
<item>0 star minimum</item>
<item>1 star minimum</item>
<item>2 stars minimum</item>
<item>3 stars minimum</item>
<item>4 stars minimum</item>
</string-array>
<string-array name="skip_min_star_rating_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
</resources>

View File

@@ -50,7 +50,7 @@
<string name="connection_alert_dialog_negative_button">Cancel</string>
<string name="connection_alert_dialog_neutral_button">Enable data saver</string>
<string name="connection_alert_dialog_positive_button">OK</string>
<string name="connection_alert_dialog_summary">Access to the Subsonic server on connections other than Wi-Fi has been restricted. To prevent this alert dialod from reappearing, disable the connection check in the app settings.</string>
<string name="connection_alert_dialog_summary">Access to the Subsonic server on connections other than Wi-Fi has been restricted. To prevent this alert dialog from reappearing, disable the connection check in the app settings.</string>
<string name="connection_alert_dialog_title">Wi-Fi not connected</string>
<string name="delete_download_storage_dialog_negative_button">Cancel</string>
<string name="delete_download_storage_dialog_positive_button">Continue</string>
@@ -274,20 +274,24 @@
<string name="settings_scrobble_title">Enable music scrobbling</string>
<string name="settings_share_title">Enable music sharing</string>
<string name="settings_sub_summary_scrobble">It\'s important to note that scrobbling also relies on the server being enabled to receive this data.</string>
<string name="settings_summary_skip_min_star_rating">When listening to an artist\'s radio, an instant mix or when shuffling all, tracks below a certain user rating will be ignored.</string>
<string name="settings_summary_replay_gain">Replay gain is a feature that allows you to adjust the volume level of audio tracks for a consistent listening experience. This setting is only effective if the track contains the necessary metadata.</string>
<string name="settings_summary_scrobble">Scrobbling is a feature that allows your device to send information about the songs you listen to the music server. This information helps create personalized recommendations based on your music preferences.</string>
<string name="settings_summary_share">Allows the user to share music via a link. The functionality must be supported and enabled server-side and is limited to individual tracks, albums and playlists.</string>
<string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.</string>
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format\" is set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_download">Download transcoded media. If enabled, the download endpoint will not be used, but the following settings. \n\n If \"Transcode format for donwloads\" is set to \"Direct download\" the bitrate of the file will not be changed.</string>
<string name="settings_summary_transcoding_estimate_content_length">When the file is transcoded on the fly, the client usually does not show the track length. It is possible to request the servers that support the functionality to estimate the duration of the track being played, but the response times may take longer.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
<string name="settings_sync_starred_tracks_for_offline_use_title">Sync starred tracks for offline use</string>
<string name="settings_theme">Theme</string>
<string name="settings_title_data">Data</string>
<string name="settings_title_general">General</string>
<string name="settings_title_rating">Rating</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_scrobble">Scrobble</string>
<string name="settings_title_skip_min_star_rating">Ignore tracks based on rating</string>
<string name="settings_title_skip_min_star_rating_dialog">Songs with a rating of:</string>
<string name="settings_title_share">Share</string>
<string name="settings_title_syncing">Syncing</string>
<string name="settings_title_transcoding">Transcoding</string>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="media" />
</automotiveApp>

View File

@@ -232,6 +232,19 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_rating">
<SeekBarPreference
android:key="min_star_rating"
app:defaultValue="0"
app:min="0"
android:max="4"
app:seekBarIncrement="1"
app:showSeekBarValue="true"
app:summary="@string/settings_summary_skip_min_star_rating"
app:title="@string/settings_title_skip_min_star_rating" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_scrobble">
<Preference
app:selectable="false"

View File

@@ -3,4 +3,6 @@
<locale android:name="en"/> <!-- English -->
<locale android:name="de-DE"/> <!-- German -->
<locale android:name="fr-FR"/> <!-- French -->
<locale android:name="zh-CN"/> <!-- Simplified Chinese-->
<locale android:name="ko-KR"/> <!-- Korean-->
</locale-config>

View File

@@ -3,11 +3,7 @@
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<debug-overrides cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,467 @@
package com.cappielloantonio.tempo.service
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.session.LibraryResult
import com.cappielloantonio.tempo.repository.AutomotiveRepository
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
object MediaBrowserTree {
private lateinit var automotiveRepository: AutomotiveRepository
private var treeNodes: MutableMap<String, MediaItemNode> = mutableMapOf()
private var isInitialized = false
// Root
private const val ROOT_ID = "[rootID]"
// First level
private const val HOME_ID = "[homeID]"
private const val LIBRARY_ID = "[libraryID]"
private const val OTHER_ID = "[otherID]"
// Second level HOME_ID
private const val MOST_PLAYED_ID = "[mostPlayedID]"
private const val LAST_PLAYED_ID = "[lastPlayedID]"
private const val RECENTLY_ADDED_ID = "[recentlyAddedID]"
private const val MADE_FOR_YOU_ID = "[madeForYouID]"
private const val STARRED_TRACKS_ID = "[starredTracksID]"
private const val STARRED_ALBUMS_ID = "[starredAlbumsID]"
private const val STARRED_ARTISTS_ID = "[starredArtistsID]"
// Second level LIBRARY_ID
private const val FOLDER_ID = "[folderID]"
private const val INDEX_ID = "[indexID]"
private const val DIRECTORY_ID = "[directoryID]"
private const val PLAYLIST_ID = "[playlistID]"
// Second level OTHER_ID
private const val PODCAST_ID = "[podcastID]"
private const val RADIO_ID = "[radioID]"
private const val ALBUM_ID = "[albumID]"
private const val ARTIST_ID = "[artistID]"
private class MediaItemNode(val item: MediaItem) {
private val children: MutableList<MediaItem> = ArrayList()
fun addChild(childID: String) {
this.children.add(treeNodes[childID]!!.item)
}
fun getChildren(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val listenableFuture = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
val libraryResult = LibraryResult.ofItemList(children, null)
listenableFuture.set(libraryResult)
return listenableFuture
}
}
private fun buildMediaItem(
title: String,
mediaId: String,
isPlayable: Boolean,
isBrowsable: Boolean,
mediaType: @MediaMetadata.MediaType Int,
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
album: String? = null,
artist: String? = null,
genre: String? = null,
sourceUri: Uri? = null,
imageUri: Uri? = null
): MediaItem {
val metadata =
MediaMetadata.Builder()
.setAlbumTitle(album)
.setTitle(title)
.setArtist(artist)
.setGenre(genre)
.setIsBrowsable(isBrowsable)
.setIsPlayable(isPlayable)
.setArtworkUri(imageUri)
.setMediaType(mediaType)
.build()
return MediaItem.Builder()
.setMediaId(mediaId)
.setSubtitleConfigurations(subtitleConfigurations)
.setMediaMetadata(metadata)
.setUri(sourceUri)
.build()
}
fun initialize(automotiveRepository: AutomotiveRepository) {
this.automotiveRepository = automotiveRepository
if (isInitialized) return
isInitialized = true
// Root level
treeNodes[ROOT_ID] =
MediaItemNode(
buildMediaItem(
title = "Root Folder",
mediaId = ROOT_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
// First level
treeNodes[HOME_ID] =
MediaItemNode(
buildMediaItem(
title = "Home",
mediaId = HOME_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[LIBRARY_ID] =
MediaItemNode(
buildMediaItem(
title = "Library",
mediaId = LIBRARY_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[OTHER_ID] =
MediaItemNode(
buildMediaItem(
title = "Other",
mediaId = OTHER_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[ROOT_ID]!!.addChild(HOME_ID)
treeNodes[ROOT_ID]!!.addChild(LIBRARY_ID)
treeNodes[ROOT_ID]!!.addChild(OTHER_ID)
// Second level HOME_ID
treeNodes[MOST_PLAYED_ID] =
MediaItemNode(
buildMediaItem(
title = "Most played",
mediaId = MOST_PLAYED_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
)
)
treeNodes[LAST_PLAYED_ID] =
MediaItemNode(
buildMediaItem(
title = "Last played",
mediaId = LAST_PLAYED_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
)
)
treeNodes[RECENTLY_ADDED_ID] =
MediaItemNode(
buildMediaItem(
title = "Recently added",
mediaId = RECENTLY_ADDED_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
)
)
treeNodes[MADE_FOR_YOU_ID] =
MediaItemNode(
buildMediaItem(
title = "Made for you",
mediaId = MADE_FOR_YOU_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
)
)
treeNodes[STARRED_TRACKS_ID] =
MediaItemNode(
buildMediaItem(
title = "Starred tracks",
mediaId = STARRED_TRACKS_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[STARRED_ALBUMS_ID] =
MediaItemNode(
buildMediaItem(
title = "Starred albums",
mediaId = STARRED_ALBUMS_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
)
)
treeNodes[STARRED_ARTISTS_ID] =
MediaItemNode(
buildMediaItem(
title = "Starred artists",
mediaId = STARRED_ARTISTS_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
)
)
treeNodes[HOME_ID]!!.addChild(MOST_PLAYED_ID)
treeNodes[HOME_ID]!!.addChild(LAST_PLAYED_ID)
treeNodes[HOME_ID]!!.addChild(RECENTLY_ADDED_ID)
treeNodes[HOME_ID]!!.addChild(MADE_FOR_YOU_ID)
treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID)
treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID)
treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID)
// Second level LIBRARY_ID
treeNodes[FOLDER_ID] =
MediaItemNode(
buildMediaItem(
title = "Folders",
mediaId = FOLDER_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[PLAYLIST_ID] =
MediaItemNode(
buildMediaItem(
title = "Playlists",
mediaId = PLAYLIST_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
)
)
treeNodes[LIBRARY_ID]!!.addChild(FOLDER_ID)
treeNodes[LIBRARY_ID]!!.addChild(PLAYLIST_ID)
// Second level OTHER_ID
treeNodes[PODCAST_ID] =
MediaItemNode(
buildMediaItem(
title = "Podcasts",
mediaId = PODCAST_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS
)
)
treeNodes[RADIO_ID] =
MediaItemNode(
buildMediaItem(
title = "Radio stations",
mediaId = RADIO_ID,
isPlayable = false,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS
)
)
treeNodes[OTHER_ID]!!.addChild(PODCAST_ID)
treeNodes[OTHER_ID]!!.addChild(RADIO_ID)
}
fun getRootItem(): MediaItem {
return treeNodes[ROOT_ID]!!.item
}
fun getChildren(
id: String
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return when (id) {
ROOT_ID -> treeNodes[ROOT_ID]?.getChildren()!!
HOME_ID -> treeNodes[HOME_ID]?.getChildren()!!
LIBRARY_ID -> treeNodes[LIBRARY_ID]?.getChildren()!!
OTHER_ID -> treeNodes[OTHER_ID]?.getChildren()!!
MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 100)
LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 100)
RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100)
MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id)
STARRED_TRACKS_ID -> automotiveRepository.starredSongs
STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id)
STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id)
FOLDER_ID -> automotiveRepository.getMusicFolders(id)
PLAYLIST_ID -> automotiveRepository.getPlaylists(id)
PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100)
RADIO_ID -> automotiveRepository.internetRadioStations
else -> {
if (id.startsWith(MOST_PLAYED_ID)) {
return automotiveRepository.getAlbumTracks(
id.removePrefix(
MOST_PLAYED_ID
)
)
}
if (id.startsWith(LAST_PLAYED_ID)) {
return automotiveRepository.getAlbumTracks(
id.removePrefix(
LAST_PLAYED_ID
)
)
}
if (id.startsWith(RECENTLY_ADDED_ID)) {
return automotiveRepository.getAlbumTracks(
id.removePrefix(
RECENTLY_ADDED_ID
)
)
}
if (id.startsWith(MADE_FOR_YOU_ID)) {
return automotiveRepository.getMadeForYou(
id.removePrefix(
MADE_FOR_YOU_ID
),
20
)
}
if (id.startsWith(STARRED_ALBUMS_ID)) {
return automotiveRepository.getAlbumTracks(
id.removePrefix(
STARRED_ALBUMS_ID
)
)
}
if (id.startsWith(STARRED_ARTISTS_ID)) {
return automotiveRepository.getArtistAlbum(
STARRED_ALBUMS_ID,
id.removePrefix(
STARRED_ARTISTS_ID
)
)
}
if (id.startsWith(FOLDER_ID)) {
return automotiveRepository.getIndexes(
INDEX_ID,
id.removePrefix(
FOLDER_ID
)
)
}
if (id.startsWith(INDEX_ID)) {
return automotiveRepository.getDirectories(
DIRECTORY_ID,
id.removePrefix(
INDEX_ID
)
)
}
if (id.startsWith(DIRECTORY_ID)) {
return automotiveRepository.getDirectories(
DIRECTORY_ID,
id.removePrefix(
DIRECTORY_ID
)
)
}
if (id.startsWith(PLAYLIST_ID)) {
return automotiveRepository.getPlaylistSongs(
id.removePrefix(
PLAYLIST_ID
)
)
}
if (id.startsWith(ALBUM_ID)) {
return automotiveRepository.getAlbumTracks(
id.removePrefix(
ALBUM_ID
)
)
}
if (id.startsWith(ARTIST_ID)) {
return automotiveRepository.getArtistAlbum(
ALBUM_ID,
id.removePrefix(
ARTIST_ID
)
)
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
}
}
}
// https://github.com/androidx/media/issues/156
fun getItems(mediaItems: List<MediaItem>): List<MediaItem> {
val updatedMediaItems = ArrayList<MediaItem>()
mediaItems.forEach {
if (it.localConfiguration?.uri != null) {
updatedMediaItems.add(it)
} else {
val sessionMediaItem = automotiveRepository.getSessionMediaItem(it.mediaId)
if (sessionMediaItem != null) {
var toAdd = automotiveRepository.getMetadatas(sessionMediaItem.timestamp!!)
val index = toAdd.indexOfFirst { mediaItem -> mediaItem.mediaId == it.mediaId }
toAdd = toAdd.subList(index, toAdd.size)
updatedMediaItems.addAll(toAdd)
}
}
}
return updatedMediaItems
}
fun search(query: String): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return automotiveRepository.search(
query,
ALBUM_ID,
ARTIST_ID
)
}
}

View File

@@ -0,0 +1,162 @@
package com.cappielloantonio.tempo.service
import android.content.Context
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.repository.AutomotiveRepository
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
open class MediaLibrarySessionCallback(
context: Context,
automotiveRepository: AutomotiveRepository
) :
MediaLibraryService.MediaLibrarySession.Callback {
init {
MediaBrowserTree.initialize(automotiveRepository)
}
private val customLayoutCommandButtons: List<CommandButton> = listOf(
CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
.setSessionCommand(
SessionCommand(
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY
)
).setIconResId(R.drawable.exo_icon_shuffle_off).build(),
CommandButton.Builder()
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
.setSessionCommand(
SessionCommand(
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY
)
).setIconResId(R.drawable.exo_icon_shuffle_on).build()
)
@OptIn(UnstableApi::class)
val mediaNotificationSessionCommands =
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
.also { builder ->
customLayoutCommandButtons.forEach { commandButton ->
commandButton.sessionCommand?.let { builder.add(it) }
}
}.build()
@OptIn(UnstableApi::class)
override fun onConnect(
session: MediaSession, controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
if (session.isMediaNotificationController(controller) || session.isAutomotiveController(
controller
) || session.isAutoCompanionController(controller)
) {
val customLayout =
customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(mediaNotificationSessionCommands)
.setCustomLayout(ImmutableList.of(customLayout)).build()
}
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
@OptIn(UnstableApi::class)
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
session.player.shuffleModeEnabled = true
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[1])
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
session.player.shuffleModeEnabled = false
session.setCustomLayout(
session.mediaNotificationControllerInfo!!,
ImmutableList.of(customLayoutCommandButtons[0])
)
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
}
override fun onGetLibraryRoot(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaBrowserTree.getRootItem(), params))
}
override fun onGetChildren(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return MediaBrowserTree.getChildren(parentId)
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
return super.onAddMediaItems(
mediaSession,
controller,
MediaBrowserTree.getItems(mediaItems)
)
}
override fun onSearch(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
session.notifySearchResultChanged(browser, query, 60, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetSearchResult(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
page: Int,
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return MediaBrowserTree.search(query)
}
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
}

View File

@@ -1,21 +1,23 @@
package com.cappielloantonio.tempo.service
import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.os.Bundle
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.*
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.*
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.repository.AutomotiveRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
@@ -24,33 +26,19 @@ import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var automotiveRepository: AutomotiveRepository
private lateinit var player: ExoPlayer
private lateinit var castPlayer: CastPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
override fun onCreate() {
super.onCreate()
initializeCustomCommands()
initializeRepository()
initializePlayer()
initializeCastPlayer()
initializeMediaLibrarySession()
@@ -66,140 +54,21 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
return mediaLibrarySession
}
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
override fun onDestroy() {
releasePlayer()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { commandButton ->
// TODO: Aggiungere i comandi personalizzati
// commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
player.shuffleModeEnabled = true
customLayout = ImmutableList.of(customCommands[1])
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
player.shuffleModeEnabled = false
customLayout = ImmutableList.of(customCommands[0])
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
/* override fun onGetLibraryRoot(
session: MediaLibrarySession,
browser: ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
if (params != null && params.isRecent) {
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED))
}
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
}
override fun onGetItem(
session: MediaLibrarySession,
browser: ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
val item =
MediaItemTree.getItem(mediaId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
}
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
session.notifyChildrenChanged(browser, parentId, children.size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetChildren(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}*/
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems = mediaItems.map {
it.buildUpon()
.setUri(it.requestMetadata.mediaUri)
.setMediaMetadata(it.mediaMetadata)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.build()
}
return Futures.immediateFuture(updatedMediaItems)
}
}
private fun initializeCustomCommands() {
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
private fun initializeRepository() {
automotiveRepository = AutomotiveRepository()
}
private fun initializePlayer() {
@@ -230,13 +99,13 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
}
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback)
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.setSessionActivity(sessionActivityPendingIntent)
.build()
}
if (!customLayout.isEmpty()) {
mediaLibrarySession.setCustomLayout(customLayout)
}
private fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
return MediaLibrarySessionCallback(this, automotiveRepository)
}
private fun initializePlayerListener() {
@@ -316,25 +185,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
if (this::castPlayer.isInitialized) castPlayer.release()
player.release()
mediaLibrarySession.release()
}
@SuppressLint("PrivateResource")
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
automotiveRepository.deleteMetadata()
clearListener()
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)

View File

@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.2'
classpath 'com.android.tools.build:gradle:8.1.4'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0'
}
}