diff --git a/.github/ISSUE_TEMPLATE/bug-crash-report.yml b/.github/ISSUE_TEMPLATE/bug-crash-report.yml index abc516401..094d55e76 100644 --- a/.github/ISSUE_TEMPLATE/bug-crash-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-crash-report.yml @@ -34,6 +34,7 @@ body: attributes: label: What android version do you use? options: + - Android 15 - Android 14 - Android 13 - Android 12L diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9b7aae1cd..59383ae5d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -14,23 +14,25 @@ jobs: - name: Install ninja-build run: sudo apt-get install -y ninja-build - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clone submodules run: git submodule update --init --recursive --remote - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Test app with Gradle - run: ./gradlew app:testDebug + - name: Check formatting with spotless + run: ./gradlew spotlessCheck + - name: Test musikr with Gradle + run: ./gradlew musikr:testDebug - name: Build debug APK with Gradle run: ./gradlew app:packageDebug - name: Upload debug APK artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v4 with: name: Auxio_Canary path: ./app/build/outputs/apk/debug/app-debug.apk diff --git a/.gitignore b/.gitignore index c03b3271b..d461864a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ captures/ .externalNativeBuild *.iml .cxx +.kotlin diff --git a/.gitmodules b/.gitmodules index 552a758f6..1e85e13c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,8 @@ [submodule "media"] path = media url = https://github.com/OxygenCobalt/media.git + +[submodule "musikr/src/main/cpp/taglib"] + path = musikr/src/main/cpp/taglib + url = https://github.com/taglib/taglib.git + tag = ee1931b diff --git a/CHANGELOG.md b/CHANGELOG.md index 8901e55c6..8e08ae917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 4.0.0 + +#### What's New +- A total user interface refresh based on the latest Material Design specs + - New theme palettes + - Improved designs for playback and detail views + - New app branding and icon + - Refreshed round mode + - Less intrusive music loading indicators +- **Musikr**, a brand new music loading system + - Directly accesses user files rather than unreliable media database + - Uses faster and more capable native tag parsing + - Stores cover data on-device for fast and high-quality access + - New interpretation system with many quality-of-life improvements +- Android 15 support + +#### What's Improved +- Initial music loading is signifigantly faster and less resource intensive +- Album grouping no longer done with artist +- MusicBrainz IDs will no longer split albums/artists in less tagged libraries +- M3U playlist file name is now proposed if one cannot be found within the file +- Duration is now parsed from certain files that previously could not be parsed +- ID3v2 tags are now parsed from WAV files +- NN/TT tracks/discs are now handled in Vorbis +- Music library will is less likely to fail to respond to updates +- Hidden audio files can now be loaded +- Sorting songs by date now uses songs date first, before the earliest album date +- Added working layouts for small split-screen form factors +- Added fast scrolling in detail views +- Added ability to make issues and make feedback e-mails in-app + +#### What's Fixed +- Fixed playback sheet flickering on warm start +- No longer possible to save a sort with no direction specified +- Fixed inconsistent corner radii in widget +- Possibly fixed foreground start music loading failures +- Fixed playlist view not exiting on deletion + +#### What's Changed +- Date added is now local to when the app discovers the file and will not +persist long-term +- Songs with no album are now "Unknown album" rather than folder name +- Tab layout no longer changes depending on device configuration +- Round mode is now on by default + +#### Dev/Meta +- No longer using custom logging setup +- Music loading split off into separate musikr module + ## 3.6.3 #### What's Fixed diff --git a/app/NOTICE b/NOTICE similarity index 100% rename from app/NOTICE rename to NOTICE diff --git a/README.md b/README.md index 3e99eeb61..94a1126cc 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version Releases @@ -28,14 +28,12 @@ Auxio is a local music player with a fast, reliable UI/UX without the many usele ## Screenshots

- - - - - - - - + + + + + +

@@ -61,7 +59,7 @@ precise/original dates, sort tags, and more - Headset autoplay - Stylish widgets that automatically adapt to their size - Completely private and offline -- No rounded album covers (by default) +- No rounded album covers (if you want them) ## Permissions @@ -73,23 +71,19 @@ precise/original dates, sort tags, and more You can support Auxio's development through [my Github Sponsors page](https://github.com/sponsors/OxygenCobalt). Get the ability to prioritize features and have your profile added to the README, Release Changelogs, and even the app itself! -

$16/month supporters:

- -

-

yrliet

-

-

$8/month supporters:

- + +

## Building -Auxio relies on a custom version of Media3 that enables some extra features. This adds some caveats to the build process: +Auxio relies on a patched version of Media3 that enables some extra playback features, alongside taglib for metadata +parsing. This adds some caveats to the build process: 1. `cmake` and `ninja-build` must be installed before building the project. 2. The project uses submodules, so when cloning initially, use `git clone --recurse-submodules` to properly download the external code. diff --git a/app/build.gradle b/app/build.gradle index 66642458e..a5c6bf437 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,6 @@ plugins { id "com.android.application" id "kotlin-android" id "androidx.navigation.safeargs.kotlin" - id "com.diffplug.spotless" id "kotlin-parcelize" id "dagger.hilt.android.plugin" id "kotlin-kapt" @@ -11,21 +10,19 @@ plugins { } android { - compileSdk 34 - // NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify - // it here so that binary stripping will work. - // TODO: Eventually you might just want to start vendoring the FFMpeg extension so the - // NDK use is unified - ndkVersion "26.3.11579264" + compileSdk 35 + // Auxio implicitly depends on the native modules, explicitly specify it + // here so the libraries are still stripped. + ndkVersion ndk_version namespace "org.oxycblt.auxio" defaultConfig { applicationId namespace - versionName "3.6.3" - versionCode 53 + versionName "4.0.0" + versionCode 59 - minSdk 24 - targetSdk 34 + minSdk min_sdk + targetSdk target_sdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -70,6 +67,7 @@ android { buildFeatures { viewBinding true + buildConfig true } } @@ -79,16 +77,16 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - def coroutines_version = '1.7.2' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlin_coroutines_version" // --- SUPPORT --- // General - implementation "androidx.core:core-ktx:1.12.0" - implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.core:core-ktx:$core_version" + implementation "androidx.appcompat:appcompat:1.7.0" + implementation "androidx.activity:activity-ktx:1.9.3" + // noinspection GradleDependency implementation "androidx.fragment:fragment-ktx:1.6.2" // Components @@ -97,11 +95,13 @@ dependencies { // TODO: Report this issue and hope for a timely fix // noinspection GradleDependency implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "androidx.constraintlayout:constraintlayout:2.2.0" + // 1.1.0 upgrades recyclerview to 1.3.0, keep it on 1.0.0 + //noinspection GradleDependency implementation "androidx.viewpager2:viewpager2:1.0.0" // Lifecycle - def lifecycle_version = "2.7.0" + def lifecycle_version = "2.8.7" implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" @@ -121,25 +121,31 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.1" // Database - def room_version = '2.6.1' implementation "androidx.room:room-runtime:$room_version" ksp "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" + // Build + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugaring_version" + + // --- SECOND PARTY --- + + // Musikr + implementation project(":musikr") + // --- THIRD PARTY --- // Exoplayer (Vendored) implementation project(":media-lib-exoplayer") implementation project(":media-lib-decoder-ffmpeg") - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" // Image loading - implementation 'io.coil-kt:coil-base:2.4.0' + implementation 'io.coil-kt.coil3:coil-core:3.0.2' // Material // TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just // PR a fix. - implementation "com.google.android.material:material:1.10.0" + implementation "com.google.android.material:material:1.13.0-alpha07" // Dependency Injection implementation "com.google.dagger:dagger:$hilt_version" @@ -158,25 +164,4 @@ dependencies { // Fuzzy search implementation 'org.apache.commons:commons-text:1.9' - - // Testing - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' - testImplementation "junit:junit:4.13.2" - testImplementation "io.mockk:mockk:1.13.7" - testImplementation "org.robolectric:robolectric:4.11" - testImplementation 'androidx.test:core-ktx:1.5.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} - -spotless { - kotlin { - target "src/**/*.kt" - ktfmt().dropboxStyle() - licenseHeaderFile("NOTICE") - } -} - -afterEvaluate { - preDebugBuild.dependsOn spotlessApply } diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml index dd28633f5..caaf4f429 100644 --- a/app/src/debug/res/values/donottranslate.xml +++ b/app/src/debug/res/values/donottranslate.xml @@ -1,4 +1,5 @@ Auxio Debug + org.oxycblt.auxio.debug.image.CoverProvider \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 308962b34..2d0499edd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,6 +48,7 @@ android:exported="true" android:icon="@mipmap/ic_launcher" android:launchMode="singleTask" + android:allowCrossUidActivitySwitchFromBelow="false" android:roundIcon="@mipmap/ic_launcher" android:windowSoftInputMode="adjustPan"> @@ -92,12 +93,22 @@ android:foregroundServiceType="mediaPlayback" android:icon="@mipmap/ic_launcher" android:exported="true" - android:roundIcon="@mipmap/ic_launcher"> + android:roundIcon="@mipmap/ic_launcher" + tools:ignore="ExportedService"> + + + - + + android:toYDelta="100%p" + tools:ignore="PrivateResource" /> diff --git a/app/src/main/res/color/fill_icon_bg.xml b/app/src/main/res/color/fill_icon_bg.xml deleted file mode 100644 index 916db1e7d..000000000 --- a/app/src/main/res/color/fill_icon_bg.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/overlay_stroke.xml b/app/src/main/res/color/overlay_stroke.xml deleted file mode 100644 index 21337acc3..000000000 --- a/app/src/main/res/color/overlay_stroke.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/color/sel_cover_bg.xml b/app/src/main/res/color/sel_cover_bg.xml index a35d5a7c5..5858fbefa 100644 --- a/app/src/main/res/color/sel_cover_bg.xml +++ b/app/src/main/res/color/sel_cover_bg.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/sel_on_cover_bg.xml b/app/src/main/res/color/sel_on_cover_bg.xml index dcc29354b..705c80c3d 100644 --- a/app/src/main/res/color/sel_on_cover_bg.xml +++ b/app/src/main/res/color/sel_on_cover_bg.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/sel_track.xml b/app/src/main/res/color/sel_track.xml deleted file mode 100644 index e3a847564..000000000 --- a/app/src/main/res/color/sel_track.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_album_48.xml b/app/src/main/res/drawable/ic_album_48.xml new file mode 100644 index 000000000..b06d7c0d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_album_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_artist_48.xml b/app/src/main/res/drawable/ic_artist_48.xml new file mode 100644 index 000000000..7bd67d49a --- /dev/null +++ b/app/src/main/res/drawable/ic_artist_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_email_24.xml b/app/src/main/res/drawable/ic_email_24.xml new file mode 100644 index 000000000..a50fe012a --- /dev/null +++ b/app/src/main/res/drawable/ic_email_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_feature_request_24.xml b/app/src/main/res/drawable/ic_feature_request_24.xml new file mode 100644 index 000000000..1765191ed --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_request_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_genre_48.xml b/app/src/main/res/drawable/ic_genre_48.xml new file mode 100644 index 000000000..27851f420 --- /dev/null +++ b/app/src/main/res/drawable/ic_genre_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index c16ace719..2011ea038 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -6,8 +6,35 @@ android:viewportHeight="108"> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 7a0184ca7..3a7b2746b 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,9 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_pause_48.xml b/app/src/main/res/drawable/ic_pause_48.xml new file mode 100644 index 000000000..76e9b9f6e --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play_48.xml b/app/src/main/res/drawable/ic_play_48.xml new file mode 100644 index 000000000..d163cd960 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_playlist_48.xml b/app/src/main/res/drawable/ic_playlist_48.xml new file mode 100644 index 000000000..780039011 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_repeat_on_24.xml b/app/src/main/res/drawable/ic_repeat_on_24.xml index 592f3d843..256407c6e 100644 --- a/app/src/main/res/drawable/ic_repeat_on_24.xml +++ b/app/src/main/res/drawable/ic_repeat_on_24.xml @@ -1,10 +1,10 @@ - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + diff --git a/app/src/main/res/drawable/ic_repeat_one_24.xml b/app/src/main/res/drawable/ic_repeat_one_24.xml index 307b27ecf..124138ff0 100644 --- a/app/src/main/res/drawable/ic_repeat_one_24.xml +++ b/app/src/main/res/drawable/ic_repeat_one_24.xml @@ -1,10 +1,10 @@ - + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + diff --git a/app/src/main/res/drawable/ic_shuffle_on_24.xml b/app/src/main/res/drawable/ic_shuffle_on_24.xml index 52d82252f..a6dcd0ec4 100644 --- a/app/src/main/res/drawable/ic_shuffle_on_24.xml +++ b/app/src/main/res/drawable/ic_shuffle_on_24.xml @@ -1,11 +1,10 @@ - - + android:viewportHeight="960" + android:tint="?attr/colorPrimary"> + diff --git a/app/src/main/res/drawable/ic_skip_next_40.xml b/app/src/main/res/drawable/ic_skip_next_40.xml new file mode 100644 index 000000000..28ca513fc --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next_40.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_skip_prev_40.xml b/app/src/main/res/drawable/ic_skip_prev_40.xml new file mode 100644 index 000000000..d21330db7 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_prev_40.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_song_48.xml b/app/src/main/res/drawable/ic_song_48.xml new file mode 100644 index 000000000..0da40bce7 --- /dev/null +++ b/app/src/main/res/drawable/ic_song_48.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_splash_anim.xml b/app/src/main/res/drawable/ic_splash_anim.xml index d71c04457..bf2bb92d2 100644 --- a/app/src/main/res/drawable/ic_splash_anim.xml +++ b/app/src/main/res/drawable/ic_splash_anim.xml @@ -1,28 +1,97 @@ - - - - + android:width="432dp" + android:height="432dp" + android:viewportWidth="432" + android:viewportHeight="432"> + android:name="bg" + android:pivotX="56" + android:pivotY="56"> + + + + + + + + + + + + + + + + + + + + + + + @@ -30,37 +99,67 @@ - - - - + - + + android:valueType="floatType" + tools:ignore="PrivateResource" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/sel_playing_state_48.xml b/app/src/main/res/drawable/sel_playing_state_48.xml new file mode 100644 index 000000000..d9b283a03 --- /dev/null +++ b/app/src/main/res/drawable/sel_playing_state_48.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_popup.xml b/app/src/main/res/drawable/ui_popup.xml new file mode 100644 index 000000000..14e0b82ad --- /dev/null +++ b/app/src/main/res/drawable/ui_popup.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_remote_fab_container_paused.xml b/app/src/main/res/drawable/ui_remote_fab_container_paused.xml index 82362866f..7e9d09828 100644 --- a/app/src/main/res/drawable/ui_remote_fab_container_paused.xml +++ b/app/src/main/res/drawable/ui_remote_fab_container_paused.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_remote_fab_container_playing.xml b/app/src/main/res/drawable/ui_remote_fab_container_playing.xml index f5e00b30c..9090ea9b3 100644 --- a/app/src/main/res/drawable/ui_remote_fab_container_playing.xml +++ b/app/src/main/res/drawable/ui_remote_fab_container_playing.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_scroll_thumb.xml b/app/src/main/res/drawable/ui_scroll_thumb.xml index 5802c85a9..19a93b84c 100644 --- a/app/src/main/res/drawable/ui_scroll_thumb.xml +++ b/app/src/main/res/drawable/ui_scroll_thumb.xml @@ -4,13 +4,7 @@ android:tint="?attr/colorSecondary"> - + android:width="4dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout-h360dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml new file mode 100644 index 000000000..12bb1598b --- /dev/null +++ b/app/src/main/res/layout-h360dp/fragment_detail.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-h360dp/fragment_playback_panel.xml similarity index 64% rename from app/src/main/res/layout-sw600dp/fragment_playback_panel.xml rename to app/src/main/res/layout-h360dp/fragment_playback_panel.xml index 21152be5a..b73de1fd9 100644 --- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h360dp/fragment_playback_panel.xml @@ -12,66 +12,82 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/toolbar_playback" + app:titleCentered="true" + app:subtitleCentered="true" + app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" + app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:navigationIcon="@drawable/ic_down_24" app:title="@string/lbl_playback" tools:subtitle="@string/lbl_all_songs" /> + - - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" + app:layout_constraintVertical_chainStyle="packed" /> + + + + + + + + + + + + + @@ -79,10 +95,12 @@ android:id="@+id/playback_controls_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_medium" - android:layout_marginEnd="@dimen/spacing_medium" + android:layout_marginStart="@dimen/spacing_mid_medium" + android:layout_marginEnd="@dimen/spacing_mid_medium" android:layout_marginBottom="@dimen/spacing_medium" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + tools:icon="@drawable/ic_play_24" /> + + diff --git a/app/src/main/res/layout-h480dp/fragment_detail.xml b/app/src/main/res/layout-h480dp/fragment_detail.xml new file mode 100644 index 000000000..a14df91c8 --- /dev/null +++ b/app/src/main/res/layout-h480dp/fragment_detail.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 4157231ac..bcf2e70a8 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -13,7 +13,11 @@ app:layout_constraintTop_toTopOf="parent" app:menu="@menu/toolbar_playback" app:navigationIcon="@drawable/ic_down_24" + app:subtitleCentered="true" + app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:title="@string/lbl_playback" + app:titleCentered="true" + app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" tools:subtitle="@string/lbl_all_songs" /> + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" + app:layout_constraintVertical_chainStyle="packed" /> - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" + app:layout_constraintVertical_bias="1.0" + app:layout_constraintVertical_chainStyle="packed"> + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/playback_info_container" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintWidth_max="400dp"> - - \ No newline at end of file + diff --git a/app/src/main/res/layout-h480dp/item_playback_song.xml b/app/src/main/res/layout-h480dp/item_playback_song.xml deleted file mode 100644 index 9ce0bcf47..000000000 --- a/app/src/main/res/layout-h480dp/item_playback_song.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-h600dp/item_detail_header.xml b/app/src/main/res/layout-h600dp/item_detail_header.xml deleted file mode 100644 index cdaf0e484..000000000 --- a/app/src/main/res/layout-h600dp/item_detail_header.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/item_detail_header.xml b/app/src/main/res/layout-land/item_detail_header.xml deleted file mode 100644 index 432f6dc3e..000000000 --- a/app/src/main/res/layout-land/item_detail_header.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-sw600dp/fragment_detail.xml b/app/src/main/res/layout-sw600dp/fragment_detail.xml new file mode 100644 index 000000000..e2eab4bb5 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/fragment_detail.xml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_detail_header.xml b/app/src/main/res/layout-sw600dp/item_detail_header.xml deleted file mode 100644 index 4b354bc58..000000000 --- a/app/src/main/res/layout-sw600dp/item_detail_header.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_playback_song.xml b/app/src/main/res/layout-sw600dp/item_playback_song.xml deleted file mode 100644 index 9ce0bcf47..000000000 --- a/app/src/main/res/layout-sw600dp/item_playback_song.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-sw840dp/item_detail_header.xml b/app/src/main/res/layout-sw840dp/item_detail_header.xml deleted file mode 100644 index d66af44bb..000000000 --- a/app/src/main/res/layout-sw840dp/item_detail_header.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-w600dp/fragment_detail.xml b/app/src/main/res/layout-w600dp/fragment_detail.xml new file mode 100644 index 000000000..12bb1598b --- /dev/null +++ b/app/src/main/res/layout-w600dp/fragment_detail.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp-land/fragment_main.xml b/app/src/main/res/layout-w720dp/fragment_main.xml similarity index 53% rename from app/src/main/res/layout-w600dp-land/fragment_main.xml rename to app/src/main/res/layout-w720dp/fragment_main.xml index 15beea699..c7cd805f2 100644 --- a/app/src/main/res/layout-w600dp-land/fragment_main.xml +++ b/app/src/main/res/layout-w720dp/fragment_main.xml @@ -7,22 +7,77 @@ android:layout_height="match_parent" android:background="?attr/colorSurface"> - + app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"> - + + + + + + + + + + + + + + + + + @@ -71,10 +127,12 @@ android:layout_weight="1" /> - - + diff --git a/app/src/main/res/layout/dialog_error_details.xml b/app/src/main/res/layout/dialog_error_details.xml index e19d930ab..bd5317a53 100644 --- a/app/src/main/res/layout/dialog_error_details.xml +++ b/app/src/main/res/layout/dialog_error_details.xml @@ -40,7 +40,7 @@ android:hyphenationFrequency="none" android:paddingStart="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium" - android:paddingEnd="@dimen/size_copy_button" + android:paddingEnd="@dimen/size_touchable_large" android:paddingBottom="@dimen/spacing_medium" android:typeface="monospace" tools:text="Stack trace here" /> diff --git a/app/src/main/res/layout/dialog_menu.xml b/app/src/main/res/layout/dialog_menu.xml index 1a5a7a605..922c6df18 100644 --- a/app/src/main/res/layout/dialog_menu.xml +++ b/app/src/main/res/layout/dialog_menu.xml @@ -24,7 +24,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_music_locations.xml b/app/src/main/res/layout/dialog_music_locations.xml new file mode 100644 index 000000000..8f015dec3 --- /dev/null +++ b/app/src/main/res/layout/dialog_music_locations.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_pre_amp.xml b/app/src/main/res/layout/dialog_pre_amp.xml index 24ca0a85e..9ad5a99b9 100644 --- a/app/src/main/res/layout/dialog_pre_amp.xml +++ b/app/src/main/res/layout/dialog_pre_amp.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/spacing_large" android:gravity="center" - android:minWidth="@dimen/size_pre_amp_ticker" + android:minWidth="@dimen/size_touchable_medium" android:textAppearance="@style/TextAppearance.Auxio.BodySmall" app:layout_constraintBottom_toBottomOf="@+id/with_tags_slider" app:layout_constraintEnd_toEndOf="parent" @@ -77,7 +77,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/spacing_large" android:gravity="center" - android:minWidth="@dimen/size_pre_amp_ticker" + android:minWidth="@dimen/size_touchable_medium" android:textAppearance="@style/TextAppearance.Auxio.BodySmall" app:layout_constraintBottom_toBottomOf="@+id/without_tags_slider" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 787fcb546..d574f8e79 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -204,6 +204,8 @@ + + + + + + + + + + + + + + + - - + android:layout_height="match_parent" + app:expandedTitleMargin="0dp" + app:forceApplySystemWindowInsetTop="false" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:titleEnabled="false" + app:toolbarId="@+id/detail_toolbar"> - + android:layout_gravity="bottom" + android:paddingStart="@dimen/spacing_medium" + android:paddingTop="?attr/actionBarSize" + android:paddingEnd="@dimen/spacing_medium" + app:layout_collapseMode="parallax" + app:layout_collapseParallaxMultiplier="0.85"> - + + + + + + + + + + + + + android:layout_gravity="bottom" + app:layout_collapseMode="pin" /> - + app:layout_collapseMode="pin"> - + - - + + + + + + + + + + + + + + + + + + + + + + + + tools:listitem="@layout/item_song" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index c92bab632..314aaae15 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -65,119 +65,36 @@ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" tools:layout="@layout/fragment_home_list" /> - + android:layout_gravity="top|start" + android:transitionGroup="true" + android:visibility="invisible" + android:layout_margin="@dimen/spacing_medium"> - + android:layout_margin="@dimen/spacing_tiny" /> - + - - - - - - - - - - - - - - - - - + - - - - - - - - diff --git a/app/src/main/res/layout/fragment_home_list.xml b/app/src/main/res/layout/fragment_home_list.xml index 138fef24c..cb4102b30 100644 --- a/app/src/main/res/layout/fragment_home_list.xml +++ b/app/src/main/res/layout/fragment_home_list.xml @@ -1,8 +1,57 @@ - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index a4c2804a9..9c4de0ad4 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -8,26 +8,75 @@ android:background="?attr/colorSurface" android:transitionGroup="true"> - + app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"> + + + + + + + + + + + + + + + android:layout_height="match_parent" + android:alpha="0" + android:background="?attr/colorSurfaceContainerLow" /> + android:layout_height="match_parent" + android:visibility="invisible" /> diff --git a/app/src/main/res/layout/fragment_playback_panel.xml b/app/src/main/res/layout/fragment_playback_panel.xml index 050d2b86b..fa845fb33 100644 --- a/app/src/main/res/layout/fragment_playback_panel.xml +++ b/app/src/main/res/layout/fragment_playback_panel.xml @@ -13,22 +13,25 @@ app:layout_constraintTop_toTopOf="parent" app:menu="@menu/toolbar_playback" app:navigationIcon="@drawable/ic_down_24" + app:subtitleCentered="true" + app:subtitleTextAppearance="@style/TextAppearance.Auxio.BodySmall" app:title="@string/lbl_playback" + app:titleCentered="true" + app:titleTextAppearance="@style/TextAppearance.Auxio.LabelLarge" tools:subtitle="@string/lbl_all_songs" /> - + app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" /> - - - - - + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/playback_cover" + app:layout_constraintTop_toBottomOf="@+id/playback_info_container"> - diff --git a/app/src/main/res/layout/item_detail_header.xml b/app/src/main/res/layout/item_detail_header.xml deleted file mode 100644 index 0ea6a4111..000000000 --- a/app/src/main/res/layout/item_detail_header.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_disc_header.xml b/app/src/main/res/layout/item_disc_header.xml index dbcc0e4f6..9559b913b 100644 --- a/app/src/main/res/layout/item_disc_header.xml +++ b/app/src/main/res/layout/item_disc_header.xml @@ -6,57 +6,49 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:paddingStart="@dimen/spacing_medium" - android:paddingTop="@dimen/spacing_mid_medium" + android:paddingTop="@dimen/spacing_small" android:paddingEnd="@dimen/spacing_medium" - android:paddingBottom="@dimen/spacing_mid_medium"> + android:paddingBottom="@dimen/spacing_small"> - - - - - + app:tint="@color/sel_on_cover_bg" + tools:ignore="ContentDescription" /> + tools:text="Part 1" /> diff --git a/app/src/main/res/layout/item_music_dir.xml b/app/src/main/res/layout/item_music_location.xml similarity index 89% rename from app/src/main/res/layout/item_music_dir.xml rename to app/src/main/res/layout/item_music_location.xml index ae1082de6..45d427155 100644 --- a/app/src/main/res/layout/item_music_dir.xml +++ b/app/src/main/res/layout/item_music_location.xml @@ -10,7 +10,7 @@ android:paddingBottom="@dimen/spacing_small"> diff --git a/app/src/main/res/layout/item_new_music_location.xml b/app/src/main/res/layout/item_new_music_location.xml new file mode 100644 index 000000000..2fd5338ed --- /dev/null +++ b/app/src/main/res/layout/item_new_music_location.xml @@ -0,0 +1,17 @@ + + diff --git a/app/src/main/res/layout/item_playback_song.xml b/app/src/main/res/layout/item_playback_song.xml deleted file mode 100644 index 3e8c0c6a1..000000000 --- a/app/src/main/res/layout/item_playback_song.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_scroll_thumb.xml b/app/src/main/res/layout/view_scroll_thumb.xml new file mode 100644 index 000000000..774195cab --- /dev/null +++ b/app/src/main/res/layout/view_scroll_thumb.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_default.xml b/app/src/main/res/layout/widget_default.xml index a51b36977..7c6bfe598 100644 --- a/app/src/main/res/layout/widget_default.xml +++ b/app/src/main/res/layout/widget_default.xml @@ -4,7 +4,8 @@ android:id="@android:id/background" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/colorSurface" + android:background="@drawable/ui_widget_bg_sharp" + android:backgroundTint="?attr/colorSurface" android:theme="@style/Theme.Auxio.Widget" tools:ignore="Overdraw"> diff --git a/app/src/main/res/layout/widget_docked_thin.xml b/app/src/main/res/layout/widget_docked_thin.xml index 086013dcc..9347007d2 100644 --- a/app/src/main/res/layout/widget_docked_thin.xml +++ b/app/src/main/res/layout/widget_docked_thin.xml @@ -69,8 +69,8 @@ @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ diff --git a/app/src/main/res/layout/widget_docked_wide.xml b/app/src/main/res/layout/widget_docked_wide.xml index d344799f4..646348f70 100644 --- a/app/src/main/res/layout/widget_docked_wide.xml +++ b/app/src/main/res/layout/widget_docked_wide.xml @@ -56,8 +56,8 @@ @@ -69,8 +69,8 @@ @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ @@ -108,8 +108,8 @@ diff --git a/app/src/main/res/layout/widget_pane_thin.xml b/app/src/main/res/layout/widget_pane_thin.xml index 2b2361971..8b5be1f4e 100644 --- a/app/src/main/res/layout/widget_pane_thin.xml +++ b/app/src/main/res/layout/widget_pane_thin.xml @@ -80,8 +80,8 @@ @@ -93,8 +93,8 @@ @@ -106,8 +106,8 @@ diff --git a/app/src/main/res/layout/widget_pane_wide.xml b/app/src/main/res/layout/widget_pane_wide.xml index 07c693564..14c671bf7 100644 --- a/app/src/main/res/layout/widget_pane_wide.xml +++ b/app/src/main/res/layout/widget_pane_wide.xml @@ -82,8 +82,8 @@ @@ -95,8 +95,8 @@ @@ -108,8 +108,8 @@ @@ -121,8 +121,8 @@ @@ -134,8 +134,8 @@ diff --git a/app/src/main/res/layout/widget_wafer_thin.xml b/app/src/main/res/layout/widget_wafer_thin.xml index db12288cf..385a39fe7 100644 --- a/app/src/main/res/layout/widget_wafer_thin.xml +++ b/app/src/main/res/layout/widget_wafer_thin.xml @@ -40,8 +40,8 @@ @@ -55,8 +55,8 @@ @@ -73,8 +73,8 @@ diff --git a/app/src/main/res/layout/widget_wafer_wide.xml b/app/src/main/res/layout/widget_wafer_wide.xml index d773fc5f0..bd922107a 100644 --- a/app/src/main/res/layout/widget_wafer_wide.xml +++ b/app/src/main/res/layout/widget_wafer_wide.xml @@ -39,8 +39,8 @@ @@ -60,8 +60,8 @@ @@ -75,8 +75,8 @@ @@ -93,8 +93,8 @@ @@ -114,8 +114,8 @@ diff --git a/app/src/main/res/menu/toolbar_playback.xml b/app/src/main/res/menu/toolbar_playback.xml index 27791e20a..d1a524ba5 100644 --- a/app/src/main/res/menu/toolbar_playback.xml +++ b/app/src/main/res/menu/toolbar_playback.xml @@ -6,8 +6,4 @@ android:icon="@drawable/ic_config_24" android:title="@string/lbl_equalizer" app:showAsAction="always" /> - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755bf..50ec88623 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index f22c5e0b4..c9676da47 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 2e4171a83..8c4f734a8 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 04aa61f98..ba9db7c78 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 0d56eed4f..9e519c6b3 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 389bd1376..d21d9ff65 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/navigation/inner.xml b/app/src/main/res/navigation/inner.xml index ff6f18d10..aa10ee2f9 100644 --- a/app/src/main/res/navigation/inner.xml +++ b/app/src/main/res/navigation/inner.xml @@ -87,6 +87,9 @@ + + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID" /> @@ -256,7 +259,7 @@ tools:layout="@layout/fragment_detail"> + app:argType="org.oxycblt.musikr.Music$UID" /> @@ -305,7 +308,7 @@ tools:layout="@layout/fragment_detail"> + app:argType="org.oxycblt.musikr.Music$UID" /> @@ -354,7 +357,7 @@ tools:layout="@layout/fragment_detail"> + app:argType="org.oxycblt.musikr.Music$UID" /> @@ -412,7 +415,7 @@ tools:layout="@layout/dialog_playlist_name"> + app:argType="org.oxycblt.musikr.Music$UID[]" /> + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID[]" /> @@ -449,7 +452,7 @@ tools:layout="@layout/dialog_playlist_name"> + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID[]" /> @@ -482,7 +485,7 @@ tools:layout="@layout/dialog_music_choices"> + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID" /> + app:argType="org.oxycblt.musikr.Music$UID" /> + + \ No newline at end of file diff --git a/app/src/main/res/navigation/outer.xml b/app/src/main/res/navigation/outer.xml index 63adfa466..0d15cbc8a 100644 --- a/app/src/main/res/navigation/outer.xml +++ b/app/src/main/res/navigation/outer.xml @@ -34,8 +34,8 @@ android:id="@+id/audio_peferences" app:destination="@id/audio_preferences_fragment" /> + android:id="@+id/music_locations_settings" + app:destination="@id/music_locations_dialog" /> + android:id="@+id/music_locations_dialog" + android:name="org.oxycblt.auxio.music.locations.MusicSourcesDialog" + android:label="music_locations_dialog" + tools:layout="@layout/dialog_music_locations" /> تشغيل من البوم تشغيل من فنان طابور - شغل الاغنية التالية + شغل التالي أضف إلى الطابور تمت الإضافة إلى الطابور أذهب إلى الفنان أذهب إلى الالبوم - تم حفظ الحالة أضف حفظ - لا مجلد + لا مجلد حول الإصدار - عرض على الكود في Github + كود البرنامج التراخيص - تمت برمجة التطبيق من قبل الكساندر كابيهارت + الكساندر كابيهارت الإعدادات - المظهر + المظهر والاحساس السمة تلقائي فاتح مظلم نظام الألوان السمة السوداء - استخدام اللون الاسود القاتم في الوضع المظلم + استخدام سمة اللون الاسود النقي عرض تبويتات المكتبة تغيير ظهور وترتيب تبويتات المكتبة - اغلفة البوم مدورة - جعل اغلفة الابومات ذات زوايا مدورة - استخدام نشاط بديل للإشعار + وضع دائري + تفعيل الزوايا المدورة لحدات الواجهه الاضافية (يتطلب اغلفة الالبوم ان تكون مدورة) + فعالية تنبيه مخصصة صوتيات صخب الصوت تفضيل المقطع تفضيل الالبوم ديناميكي - سلوك - عند اختيار اغنية + تخصيص + عند التشغيل من المكتبة تذكر الخلط إبقاء وضع الخلط عند تشغيل اغنية جديدة تشجيع قبل التخطي للخلف @@ -71,16 +70,14 @@ ايقاف مؤقت عند التكرار ايقاف مؤقت عند تكرار تشغيل اغنية محتوى - حفظ حالة التشغيل - حفظ حالة التشغيل الحالية الآن لم يتم ايجاد موسيقى فشل تحميل الموسيقى اوكسيو يحتاج إلى صلاحيات لقراءة للاطلاع على مكتبتك للموسيقى لا يوجد تطبيق لفتح هذا الرابط - هذا المجلد غير مدعوم + هذا المجلد غير مدعوم - البحث في مكتبتك… + ابحث في مكتبتك… المقطع %d تشغيل او ايقاف مؤقت @@ -93,7 +90,7 @@ نقل اغنية من الطابور تحريك التبويت إزالة كلمة البحث - إزالة المجلد المستبعد + إزالة المجلد المستبعد ايقونة اوكسيو غلاف الالبوم غلاف الالبوم لـ %s @@ -139,46 +136,44 @@ %d ألبومات %d ألبومات - MixMix + مكس دي جي الغاء التنسيق الحجم إحصائيات المكتبة معدل البت تجميع مباشر - تجميعات + تجميعات ريمكس خصائص الاغنية معدل العينة عشوائي - تشغيل كل الاغاني بشكل عشوائي + تشغيل عشوائي للكل حسنا - اعادة الحالة تنازلي عرض الخصائص - مسح الحالة مباشر اعادة ضبط - يتم تحمل مكتبتك … + يتم تحمل مكتبتك الموسيقية … النوع - مراقبة تغييرات في مكتبتك + مراقبة تغييرات في مكتبتك الموسيقية… مراقبة مكتبة الموسيقة تحميل الموسيقى المعادل - منفصل + فرديات فردي - EP - EPs - Mixtape - تسجيل صوتي - Mixtapes + أغنية مطولة + أغاني مطولة + مكس + موسيقى تصويرية + مكسات RemixesRemixes - الموسيقى التصويرية + موسيقات تصويرية البوم مباشر - ريمكس - مؤاثرات مباشرة - مؤاثرات ريمكس + البوم ريمكس + أغنية مطولة مباشرة + اغنية مطولة ريمكس بث مباشر فردي - ريمكس منفصل + ريمكس فردي تجميعات مدة عدد الأغاني @@ -186,9 +181,9 @@ مسار تاريخ الاضافة تحميل الموسيقى - التحويل البرمجي - مزيج - Wiki + تجميع + مكسات دي جي + ويكي أغنية أتجاه أختيار @@ -209,8 +204,66 @@ تم حذف قائمة التشغيل تقرير قائمة تشغيل جديدة - معلومات خاطئة + معلومات الخطأ تم إعادة تسمية قائمة التشغيل إعادة تسمية قائمة التشغيل يظهر على + إختيار المجلد + مقاطع تجريبية + استيراد + تصدير + تصدير قائمة تشغيل + ملاحظات + سجل مشكلة على GitHub + أرسل ايميل + تبرع + تخطى الى التالي + عند التشغيل من خصائص العنصر + تشغيل من العناصر المعروظة + تشغيل من النوع + تحكم بكيفية تحميل الاموسيقى والصور + التحميل التلقائي + تجاوز الملفات الصوتية الغير موسيقية, مثل البودكاست + تعريف الفواصل التي تدل على علامات متعددة + فاصلة منقوطة(;) + نمط المسار + ألمزيد + مطلق + الداعمين + فعالية شريط تشغيل مخصصة + العطف(&) + قائمة تشغيل مستوردة + فواصل متعددة القيم + تغيير سمات والوان البرنامج + تجاوز غير الموسيقى + مقطع تجريبي + الاغاني الخاصة بك ستضهر هنا. + الفنانين الخاصين بك سيضهرون هنا. + تعديل تثبيت مستوى الصوت للاغنية + تعديل تثبيت مستوى الصوت للالبوم + تم استيراد قائمة التشغيل + تم تصدير قائمة التشغيل + تبرع للمشروع لتتم اضافة اسمك هنا! + وضع التكرار + تشغيل اغنية محددة + زائد(+) + استيراد قائمة تشغيل + قائمة تشغيل فارغة + تخصيص ادوات تحكم الواجهه و سلوكها + المسار + الترتيب الذكي + الشارحة(/) + بدء التشغيل + نسبي + إستخدام مسارات متوافقة مع نافذة + تشغيل Auxio بأستخدام الحالة المحفوظه مسبقا. اذا لم تتوفر حالة, كل الاغاني ستشغل بشكل عشوائي.\n\nتحذير:كن حذرا بالتحكم بهذة الخدمة, اذا اغلقتها وفتحتها مجددا,قد يتوقف البرنامج عن العمل. + المؤلف + الموسيقى + قوائم التشغيل الخاصة بك ستضهر هنا. + السلوك + فاصلة (,) + الالبومات الخاصة بك ستضهر هنا. + الفئات الخاصة بك ستضهر هنا. + اعادة تحميل مكتبة الموسيقى عند حصول تغيير(يتطلب تنبيه ثابت) + تحذير: استخدام هذا الاعداد قد ينتج عنه ان يتم تفسير بعض العلامات بشكل خاطئ مثل ان تحتوي على قيم متعددة. يمكن ان يتم حل هذا بتقديم الفواصل الغير مرغوبةبالشارحة الخلفية(\\). \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 51bb12461..9d6db7b73 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,6 +1,6 @@ - مشغّل موسيقى بسيط ومعقول للأندرويد + مشغّل موسيقى بسيط ومعقول للأندرويد. مراقبة مكتبة الموسيقى إعادة المحاولة منح @@ -19,8 +19,7 @@ إضافة لقائمة التشغيل إعادة ضبط إضافة مجلد - تم حفظ الحالة - يتم تحميل مكتبتك الموسيقية + جارِ تحميل مكتبتك الموسيقية… أضيفت للطابور تم إنشاء قائمة التشغيل فنانون @@ -39,7 +38,7 @@ تجميعة تجميعة مباشرة مباشر - ظهر فيه + ظهر على فنان ريميكسات نوع @@ -71,12 +70,268 @@ الحجم معدل البِت موافق - تم حذف الحالة - تمت استعادة الحالة حول الإصدار شفرة المصدر الموسوعة التراخيص إحصائيات المكتبة - \ No newline at end of file + إيقاف التشغيل + فشل تحميل الموسيقى + انقل علامة التبويب هذه + مسح استعلام البحث + صورة قائمة التشغيل لـ %s + نوع غير معروف + لا قرص + صوت MPEG-4 + أوغ الصوت + صوت ماتروسكا + برنامج ترميز الصوت المجاني بدون فقدان البيانات (FLAC) + نيلي + أزرق + أخضر عميق + جير + أصفر + بط نهري صغير + أخضر + البرتقالي + المدة الإجمالية: %s + رمادي + تم تحديد %d + -%.1f ديسيبل + + %d أغنية + %d أغاني + %d أغاني + %d أغاني + %d أغاني + %d أغاني + + إظهار فقط الفنانين المُعتمدين مباشرة على الألبوم (يعمل بشكل أفضل في المكتبات المعروفة بتوسيماتها الجيدة) + المحتوى + مجلدات + إخفاء المتعاونين + ضبط سلوك وصوت التشغيل + إعادة التشغيل قبل الانتقال للوراء + إعادة التشغيل قبل الانتقال إلى الأغنية السابقة + البقاء على التشغيل/الإيقاف عند الانتقال أو تعديل قائمة التشغيل + تذكر الإيقاف المؤقت + مسح ذاكرة التخزين المؤقت للعلامات وإعادة تحميل كامل مكتبة الموسيقى (أبطأ ولكن أكثر اكتمالًا) + يحتاج Auxio إلى إذن لقراءة مكتبتك الموسيقية + غير قادر على استيراد قائمة التشغيل من هذا الملف + لم يتم العثور على تطبيق يمكنه التعامل مع هذه المهمة + تشغيل أو إيقاف مؤقت + إيقاف التشغيل + صوت MPEG-1 + أرجواني + الأرجواني العميق + تم استيراد قائمة التشغيل + إجراء الإشعارات المخصص + الانتقال إلى التالي + غلاف الألبوم + غير قادر على تصدير قائمة التشغيل إلى هذا الملف + إزالة المجلد + رمز مساعد + %1$s، %2$s + متحرك + البومات ما قبل الإطلاق + البوم قبل الإطلاق + تسجيل تجريبي + تسجيلات تجريبية + اغاني دي جي + اغنية دي جي + المزيد + صورة التحديد + إزالة هذه الأغنية + نقل هذه الأغنية + ألبوم ريميكس + تذكر خاصية الخلط + تم تصدير قائمة التشغيل + تم حذف قائمة التشغيل + تبرع إلى المشروع لإضافة اسمك هنا! + علامات تبويب المكتبة + تغيير مرئية وترتيب علامات تبويب المكتبة + إجراء شريط التشغيل المخصص + تشغيل من العنصر المعروض + تشغيل من الألبوم + تشغيل من الفنان + تشغيل من النوع + تشغيل الأغنية بمفردها + الموسيقى + تحذير: استخدام هذا الإعداد قد يؤدي إلى تفسير بعض العلامات بشكل غير صحيح بأن لديها قيم متعددة. يمكنك حل هذه المشكلة عن طريق إضافة شريط مائل مسبوق بشرطة مائلة عكسية (\\). + استعادة + ألبومات ممتدة + البوم مصغر + ألبوم موسيقي مباشر + جاري تحميل الموسيقى + جاري تحميل الموسيقى + أغنية فردية مباشرة + اغنية فردية ريميكس + الصور + إعادة تحميل مكتبة الموسيقى، باستخدام العلامات المخزنة مؤقتًا عند الإمكان + لا توجد مجلدات + هذا المجلد غير مدعوم + خلط جميع الأغاني + ازرق سماوي + تحرير %s + تلقائي + نظام الألوان + أغنية فردية + أغاني فردية + الرمز زائد (+) + الفرز الذكي + أحمر + ازرق غامق + إعادة التحميل التلقائي + ضبط الأحرف التي تشير إلى قيم علامات متعددة + البدء التلقائي في التشغيل عند توصيل سماعة الرأس (قد لا يعمل على جميع الأجهزة) + اصوات تصويرية + التحكم في كيفية تحميل الموسيقى والصور + حافظ على خاصية الخلط عند تشغيل أغنية جديدة + سماعة الرأس تشغيل تلقائي + توحيد مستوى الصوت + إيقاف مؤقت عند التكرار + إيقاف مؤقت عند تكرار الأغنية + المسار %d + البوم اغاني ريميكس + لا توجد ألبومات + إعادة فحص الموسيقى + غلاف الألبوم لـ %s + صورة الفنان لـ %s + صورة النوع لـ %s + فنان غير معروف + استراتيجية الريبلاي جين + ضبط بدون علامات + قائمة تشغيل فارغة + الوضع الدائري + عند التشغيل من المكتبة + تشغيل من جميع الأغاني + وضع التكرار + السلوك + عند التشغيل من تفاصيل العنصر + تجاهل ملفات الصوت التي ليست موسيقى، مثل البودكاست + فواصل القيم المتعددة + أعد تحميل مكتبة الموسيقى عندما تتغير (يتطلب إشعار دائم) + استثناء غير الموسيقى + الرمز التّعجبي (&) + الفاصلة (،) + شرطة مائلة (/) + أغطية الألبومات + سريع + عالية الجودة + فرض أغطية ألبومات مربعة + قص جميع أغطية الألبومات إلى نسبة جانب 1:1 + صوت + تحذير: تغيير مكبر الصوت المسبق إلى قيمة إيجابية عالية قد يؤدي إلى ظهور ذروات صوتية على بعض المسارات الصوتية. + مكتبة + مجلدات الموسيقى + إدارة موقع تحميل الموسيقى + الانتقال إلى الأغنية الأخيرة + تغيير وضع التكرار + افتح قائمة الانتظار + الانتقال إلى الأغنية التالية + تشغيل العشوائية أو إيقافها + %d كيلو بايت في الثانية + ابحث في مكتبتك… + داكن + استخدم سمة داكنة بلون أسود نقي + المظهر + غيّر سمة وألوان التطبيق + السمة + فاتح + تفعيل الزوايا المستديرة على عناصر واجهة المستخدم الإضافية (يتطلب أن تكون أغلفة الألبومات مستديرة) + تخصيص + تفضيل الألبوم إذا كان أحد الألبومات يُشغّل + تفضيل المسار + مكبر الصوت المسبق لـ ReplayGain + تفضيل الألبوم + يتم تطبيق مكبر الصوت المسبق على التعديل الحالي أثناء التشغيل + ضبط مع العلامات + تحديث الموسيقى + لم يتم العثور على موسيقى + لا مسار + لا يوجد تشغيل الموسيقى + ترميز الصوت المتقدم (AAC) + لا يوجد تاريخ + لا أغاني + لون القرنفل + الفرز الصحيح للأسماء التي تبدأ بأرقام أو بكلمات مثل \"the\" (يعمل بشكل أفضل مع الموسيقى باللغة الإنجليزية) + إيقاف + بني + الأغاني التي تم تحميلها: %d + الألبومات المحملة: %d + الفنانون الذين تم تحميلهم: %d + قائمة التشغيل %d + جارٍ تحميل مكتبة الموسيقى الخاصة بك… (%1$d/%2$d) + هل تريد حذف %s؟ هذا لا يمكن التراجع عنها. + الأنواع المحملة: %d + القرص %d + +%.1f ديسيبل + %d هرتز + عرض + البوم موسيقي ريميكس + صوت تصويري + تم إعادة تسمية قائمة التشغيل + سمة اللون الأسود + تخصيص عناصر التحكم وسلوك واجهة المستخدم + تمت إضافته إلى قائمة التشغيل + إعدادات + الفاصلة المنقوطة (؛) + اختر المجلدات + استورد + صدّر + صدّر قائمة التشغيل + معدل العينة + نمط المسار + تحديد + الانطباعات + افتح مشكلة على GitHub + إرسل بريد إلكتروني + المؤيدون + المزيد + نسبي + مطلق + غير معروف + قائمة التشغيل استوردت + ألبوم غير معروف + افرز حسب + الاتجاه + + لا فنان + فنان + فنانان + %d فنانين + %d فنان + %d فنان + + ستظهر أغانيك هنا. + ستظهر ألبوماتك هنا. + سوف يظهر الفنانون الخاص بك هنا. + سوف تظهر قوائم تشغيلك هنا. + سوف تظهر أنواعك هنا. + تعديل الألبوم Replaygain + تعديل مسار ReplayGain + ألكساندر كيبهارت + تبرّع + المؤلف + استورد قائمة التشغيل + المسار + مجلد جديد + بدء التشغيل + معلومات الخطأ + نُسخت + إبلاغ + يبدأ Auxio باستخدام الحالة المحفوظة مسبقًا. إذا لم تتوفر حالة محفوظة، فسيتم خلط جميع الأغاني. سيبدأ التشغيل على الفور. \n\nتحذير: احرص على التحكم في هذه الخدمة، إذا قمت بإغلاقها ثم حاول استخدامها مرة أخرى، فمن المحتمل أن ينهار التطبيق. + استخدم مسارات متوافقة مع Windows + وفر المساحة + + لا ألبوم + ألبوم + ألبومان + %d ألبومات + %d ألبوم + %d ألبوم + + MPEG-4 تحتوي على %s + Apple Lossless Audio Codec (ALAC) + diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 8a0023135..5f602a887 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -3,25 +3,133 @@ Musiqi yüklənir Musiqi yüklənir Təkrar cəhd et - İcazə ver + Qəbul et Mahnılar Bütün mahnılar Albomlar Albom Canlı albom - Remiks albom + Qarışıq albom Ep-lər - Tək - Tək - Canlı tək - Remiks tək + Seçmə + Seçmə + Canlı seçmə + Qarışıq seçmə Kompilyasiya Kompilyasiyalar - Səs treki - Android üçün asan, səmərəli musiqi oynadıcı. + Səs axını + Android üçün asan, səmərəli musiqi səsləndirici. Musiqi kitabxanası yoxlanılır Canlı EP EP Remiks EP - Səs treklər + Səs axınları + Yeni pleylist + Remiks kompilyasiyası + DJ Mikslər + Canlı kompilyasiya + Demo + Demos + Canlı + Remikslər + Sənətkar + Sənətkarlar + Janr + Daha çox + Mahnı + Qarışıq lentlər + Qarışıq lent + DJ Miks + Janrlar + Pleylist + Pleylistlər + Boş pleylist + Üzərində Görünür + İdxal edilən pleylist + Sil + Axtarış + Disk + Adı dəyişdir + Düzəliş et + Hamısı + Müddət + Filtr + Tarix + Pleylisti idxal et + Pleylisti yenidən adlandır + Pleylist silinsin? + Ad + Mahnı sayı + İdxal et + İxrac et + Pleylist ixrac et + Pleylistə əlavə et + Xüsusiyyətlərə bax + Nümunə sürəti + Daha çox + Səsləndirməni başlat + İmtina et + Sıfırla + Növbə + Qarışdır + Sənətkara bax + Alboma keç + Baxış + Paylaş + Əlavə et + Səsləndir + Çeşidləmə üsulu + Göstəriş + Azalan + Axın + Əlavə edilən tarix + Artan + İndi səslənir + Format + Həcm + ReplayGain Axın Nizamlanması + ReplayGain Albom Nizamlanması + Oldu + Saxla + Çeşidlə + Tarazlayıcı + Qarışdır + Növbəti səsləndirmə + Növbəyə əlavə et + Mahnı detalları + Yol + Hamısın Qarışdır + Bit sürəti + Haqqında + Köçürüldü + Hesabat + Xəta məlumatı + Yaradıcı + Alexander Capehart + Versiya + Mənbə kodu + Viki + Kitabxana statistikası + Lisenziyalar + Seçim + Yol üsulu + Tam + Nisbi + Windows-a uyuşan yolları istifadə et + Musiqi səslənməsinə bax və idarə et + İanə ver + Dəstəkçilər + Musiqi kitabxananız yüklənir… + Musiqi kitabxananız dəyişikliklər üçün yoxlanılır… + Əks Əlaqə + E-poçt göndər + GitHub-da problem yarat + Qovluqları seç + Pleylist ixrac edildi + Pleylistə əlavə edildi + Pleylist adı dəyişdirildi + Pleylist idxal edildi + Pleylist silindi + Səsləndirmə siyahısı yaradıldı + Növbəyə əlavə edildi \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index e84a07e3d..2e83e80c0 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -16,7 +16,6 @@ Коска (,) Плюс (+) Амперсанд (&) - Ўключыць Афармленне Паўтарыць Пошук у вашай бібліятэцы… @@ -63,7 +62,6 @@ Жанры Сартаваць Дата дабаўлення - Дзяржава адноўлена Дыск Дата Працягласць @@ -91,22 +89,19 @@ Частата дыскрэтызацыі Скінуць Дадаць - Дзяржава захаваная Вікі Захаваць - Дзяржава ачышчана Версія Выкарыстоўвайце чыста чорную цёмную тэму - Падвышанай якасці (павольная загрузка) + Падвышанай якасці (павольная загрузка) Аддайце перавагу альбому, калі ён гучыць Карэкціроўка без тэгаў - Тэчкі з музыкай - Захаваць бягучы стан прайгравання + Тэчкі з музыкай Аўтапрайграванне гарнітуры Наладзьце гук і паводзіны прайгравання Заўсёды пачынаць гульню, калі падключана гарнітура (можа працаваць не на ўсіх прыладах) Паўза, калі песня паўтараецца - Выраўноўванне гучнасці ReplayGain + Выраўноўванне гучнасці Карэкціроўка з тэгамі Пераматайце назад, перш чым перайсці назад Перамотка назад, перш чым перайсці да папярэдняй песні @@ -117,33 +112,21 @@ Аддайце перавагу альбому Папярэдні ўзмацняльнік ReplayGain Бібліятэка - Кіруйце месцам загрузкі музыкі - Тэчкі - Рэжым - Выключэнні - Музыка не будзе загружана з выбраных тэчак. - Музыка будзе загружана толькі з выбраных тэчак. + Кіруйце месцам загрузкі музыкі + Тэчкі Перасканаваць музыку Абнавіць музыку Перазагрузіце музычную бібліятэку, выкарыстоўваючы па магчымасці кэшаваныя тэгі Ачысціце кэш тэгаў і цалкам перазагрузіце музычную бібліятэку (павольней, але больш поўна) - Ачысціць стан прайгравання - Немагчыма ачысціць стан - Стан прайгравання - Захаваць стан прайгравання - Аднавіць раней захаваны стан прайгравання (калі ёсць) Auxio патрабуецца дазвол на чытанне вашай музычнай бібліятэкі - Ачысціць раней захаваны стан прайгравання (калі ёсць) Музыка не знойдзена Памылка загрузкі музыкі - Няма тэчак - Гэтая папка не падтрымліваецца - Немагчыма аднавіць стан + Няма тэчак + Гэтая папка не падтрымліваецца Кампазіцыя %d Перамясціць песню ў чаргу Не знойдзена прыкладання, якое можа справіцца з гэтай задачай Прайграванне або прыпыненне - Немагчыма захаваць стан Перайсці да наступнай песні Перайсці да апошняй песні Змяніць рэжым паўтору @@ -154,7 +137,7 @@ Спыніць прайграванне Адкрыйце чаргу Ачысціць пошукавы запыт - Выдаліць тэчку + Выдаліць тэчку Вокладка альбома Вокладка альбома %s Выява выканаўцы для %s @@ -249,7 +232,6 @@ Шматзначныя раздзяляльнікі Папярэджанне: выкарыстанне гэтай налады можа прывесці да таго, што некаторыя тэгі будуць памылкова інтэрпрэтавацца як тыя, што маюць некалькі значэнняў. Вы можаце вырашыць гэта, дадаўшы да непажаданых сімвалаў-падзельнікаў зваротную касую рысу (\\). Паўза пры паўторы - Аднавіць стан прайгравання Касая рыса (/) Малюнкі Кропка з коскай (;) @@ -258,7 +240,7 @@ Вокладкі альбомаў Перамясціць гэту ўкладку Адключаны - Зыходныя (хуткая загрузка) + Зыходныя (хуткая загрузка) Аўдыё Прайграванне Канцэртны міні-альбом @@ -270,7 +252,6 @@ Вокладка плэйліст для %s Плэйліст Плэйлісты - Стварыце новы плэйліст Плэйліст %d Новы плэйліст Дадаць у плэйліст @@ -328,4 +309,25 @@ Ахвяруйце на праект, каб ваша імя было дададзена тут! Запамінаць паўзу Пакідаць прайграванне/паўзу падчас пропуску або рэдагаванні чаргі + Адкл. + Пачаць прайграванне + Запускаць auxio, выкарыстоўваючы раней захаваны стан. Калі захаваны стан недаступны, усе песні будуць ператасаваныя. Прайграванне пачнецца неадкладна. +\n +\nПапярэджанне: будзьце асцярожныя пры кіраванні гэтай службай, калі вы закрыеце яе, а затым паспрабуеце выкарыстоўваць зноў, вы, верагодна, прывядзеце да збою прыкладання. + Больш + Абярыце тэчкі + MPEG-4, які змяшчае %s + Невядомы альбом + Вашыя альбомы будуць адлюстроўвацца тут. + Тут будуць адлюстроўвацца вашыя жанры. + Новая тэчка + Apple Lossless Audio Codec (ALAC) + Невядомы + Адправіць электронны ліст + Водгукі + Вашыя песні будуць адлюстроўвацца тут. + Тут з\'явяцца вашыя выканаўцы. + Тут будуць адлюстроўвацца вашыя плэйлісты. + Эканомія месца + Паведаміць аб праблеме на GitHub \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..54296b712 --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,313 @@ + + + Зареждане на музика + Мониторинг на музикална библиотека + Опитайте отново + Песни + Песен + Всички песни + Албум + Албум на живо + Ремикс албум + Ремикс EP + Сингли + Сингъл + Ремикс сингъл + Компилации + Компилация + Компилация на живо + Ремикс компилация + Саундтраци + Саундтрак + Миксове + Микс + Демонстрации + DJ миксове + На живо + Ремикси + Появява се на + Изпълнител + Изпълнители + Жанр + Жанрове + Плейлист + Нов плейлист + Празен плейлист + Изнасяне + Внасяне + Внасяне на плейлист + Изнасяне на плейлист + Преименувай + Изтрий + Изтрий плейлист? + Редактирай + Търсене + Филтър + Всички + Име + Дата + Продължителност + Диск + Изпълни + Разбъркано + Опашка + Изпълни следваща + Добави към опашката + Добави към плейлист + Отиди на изпълнител + Отиди на албум + Виж свойства + Преглед + Сподели + Свойства на песента + Път + Формат + Размер + Битрейт + Честота на дискретизация + ReplayGain Регулиране на албума + Разбъркано + Добре + Отказ + Запази + Нулирай + Стил на пътя + Относително + Използвай съвместими с Windows пътища + Прост, рационален музикален плейър за android. + Зарежда се музика + Още + Албуми + EP на живо + Сингъл наживо + Внесен плейлист + Преименувай плейлист + Брой песни + Датата е добавена + Вид + DJ микс + Писта + Сортирай по + Еквалайзер + Демонстрация + Посока + Плейлисти + Възходящ + Низходящ + Сега се изпълнява + ReplayGain Регулиране на песента + Разбъркай всички + Добави + Относно + Абсолютно + EPs + EP + Версия + Програмен код + Wiki + Лицензи + Библиотечна статистика + Избор + Информация за грешка + Копирано + Докладвай + Автор + Alexander Capehart + Дарете + Поддръжници + Преглед и контрол на възпроизвеждането на музика + Вашата музикална библиотека се зарежда… + Наблюдение на музикалната ви библиотека за промени… + Добави към опашката + Създаден плейлист + Внесен плейлист + Преименуван плейлист + Изнесен плейлист + Изтрит плейлист + Добави към плейлист + Настройки + Виж и Усети + Промени темата и цветовете на приложението + Тема + Автоматична + Светла + Тъмна + Цветова схема + Черна тема + Използвай чисто черна тъмна тема + Кръгъл режим + Активирай заоблените ъгли на допълнителните елементи на потребителския интерфейс (изисква обложките на албумите да са заоблени) + Персонализирайте + Персонализирайте контролите и поведението на потребителския интерфейс + Дисплей + Раздели на библиотеката + Промени видимостта и реда на разделите на библиотеката + Персонализирано действие за известие + Премини към следваща + Режим на повторение + Поведение + При изпълнение от детайли на елемента + При изпълнение от библиотеката + Изпълни от албум + Изпълни от изпълнител + Изпълни от жанр + Изпълни песен сама по себе си + Запомни разбъркване + Запази функцията разбъркване при изпълнение на нова песен + Съдържание + Контролирай как да се зареждат музика и изображение + Музика + Автоматично презареждане + Презаредете музикалната библиотека, когато се промени (изисква постоянно известие) + Изключи не-музика + Разделители с множество стойности + Конфигурирай знаци, които обозначават множество стойности на тагове + Запетая (,) + Точка и запетая (;) + Наклонена черта (/) + Плюс (+) + Амперсанд (&) + Интелигентно сортиране + Обложки на албум + Изключено + Бързо + Високо качество + Функция квадратни обложки на албум + Изрежи всички корици на албуми до съотношение 1:1 + Аудио + Конфигурирай поведението на звука и възпроизвеждане + Възпроизвеждане + Автоматично пускане на слушалки + Винаги започва да възпроизвежда, когато са свързани слушалки (може да не работи на всички устройства) + Превъртете преди да преминете назад + Превъртете преди да преминете към предишната песен + Пауза при повторение + Запомни пауза + Нормализация на звука + ReplayGain стратегия + Изключено + Предоставяне + Дарете за проекта, за да бъде добавено името ви тук! + Търсене във вашата библиотека… + Персонализирано действие на лентата за възпроизвеждане + Изпълни от показания елемент + Изпълни от всички песни + Игнорирай аудио файлове, които не са музика, като подкасти + Предупреждение: Използването на тази настройка може да доведе до неправилно тълкуване на някои тагове като имащи множество стойности. Можете да разрешите това, като поставите пред нежеланите разделителни знаци обратна наклонена черта (\\). + Правилно сортиране на имена, които започват с цифри или думи като \"the\" (работи най-добре с музика на английски) + Скриване на сътрудниците + Показвай само изпълнители, които са директно посочени в албум (работи най-добре при добре маркирани библиотеки) + Изображения + Пауза когато песента се повтаря + Останете в режим на изпълнение/пауза, когато прескачате или редактирате опашка + Предпочитам песен + Предпочитам албум + Предпочитам албум, ако се пуска такъв + ReplayGain предусилвател + Предварителният усилвател се прилага към съществуващата настройка по време на възпроизвеждане + Регулиране с етикети + Корекция без етикети + Библиотека + Музикални папки + Управлявайте откъде да се зарежда музиката + Папки + Обновяване на музика + Няма намерена музика + Неуспешно зареждане на музика + Auxio се нуждае от разрешение, за да чете вашата музикална библиотека + Плейлиста не може да се изнесе в този файл + Няма намерено приложение, което да може да се справи с тази задача + Няма папки + Изпълни или пауза + Премини към следваща песен + Премини към последна песен + Промени режима на повторение + Включи или изключи разбъркване + Разбъркай всички песни + Спри възпроизвеждането + Премахни тази песен + Премести тази песен + Отвори опашката + Избери изображение + Премести този раздел + Предупреждение: Промяната на пред усилвателя на висока положителна стойност може да доведе до пикове на някои аудио записи. + Изчисти заявката за търсене + Премахни папка + Не може да се внесе плейлист от този файл + Тази папка не се поддържа + Auxio икона + Обложка на албум + Обложка на албум за %s + Изображение на изпълнител за %s + Презареди музикалната библиотека, като използвате кеширани етикети, когато е възможно + Жанрово изображение за %s + Повторно сканиране на музика + Песен %d + Изображение на плейлист за %s + Изчисти кеша на етикета и пълно презареждане на музикалната библиотека (по-бавно, но по-пълно) + Неизвестен изпълнител + Няма дата + Няма диск + Няма песен + Няма песни + Няма албуми + Няма музика за възпроизвеждане + MPEG-1 audio + MPEG-4 audio + Ogg audio + Matroska audio + Розово + Червено + Лилаво + Индиго + Free Lossless Audio Codec (FLAC) + Зелено + Наситено зелено + Лайм + Жълто + Синьо-зелено + Зеленикаво-синьо + Оранжево + Редактиране %s + Диск %d + %1$s, %2$s + Плейлист %d + +%.1f dB + Заредени песни: %d + -%.1f dB + %d kbps + Заредени албуми: %d + Заредени изпълнители: %d + Заредени жанрове: %d + Обща продължителност: %s + Наситено лилаво + Синьо + Наситено синьо + Кафяво + %d Избрано + Вашата музикална библиотека се зарежда… (%1$d/%2$d) + Изтрий %s? Това не може да бъде отменено. + + %d песен + %d песени + + Сиво + + %d изпълнител + %d изпълнители + + Динамичен + + %d албум + %d албуми + + Advanced Audio Coding (AAC) + %d Hz + Неизвестен жанр + Стартирай Auxio, използвайки предварително запазеното състояние. Ако няма налично запазено състояние, всички песни ще бъдат разбъркани. Възпроизвеждането ще започне веднага. \n \nПРЕДУПРЕЖДЕНИЕ: Бъдете внимателни, когато контролирате тази услуга, ако я затворите и след това опитате да я използвате отново, вероятно ще сринете приложението. + Стартирай възпроизвеждане + Още + Обратна връзка + Направете проблем в GitHub + Изпратете имейл + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6d70c2fce..386ec94e2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -38,7 +38,6 @@ Přidáno do fronty Přejít na umělce Přejít na album - Stav uložen OK @@ -88,8 +87,6 @@ Pozastavit při opakování Pozastavit při opakování skladby Obsah - Uložit stav přehrávání - Uložit aktuální stav přehrávání Obnovit hudbu Znovu načíst hudební knihovnu, pokud možno s použitím značek uložených v mezipaměti @@ -97,8 +94,8 @@ Načítání hudby selhalo Auxio potřebuje oprávnění ke čtení vaší hudební knihovny Nebyla nalezena žádná aplikace, která by dokázala vykonat tuto akci - Žádné složky - Tato složka není podporována + Žádné složky + Tato složka není podporována Prohledat vaší knihovnu… @@ -113,7 +110,7 @@ Přesunout tuto skladbu ve frontě Přesunout tuto kartu Vymazat hledání - Odebrat složku + Odebrat složku Ikona Auxio Obal alba Obal alba %s @@ -167,11 +164,6 @@ Náhodně Vše náhodně - Režim - Vyloučit - Z přidaných složek nebude načtena hudba. - Zahrnout - Hudba bude načtena pouze z přidaných složek. Zvuk MPEG-1 Zvuk MPEG-4 Zvuk Ogg @@ -181,7 +173,7 @@ %d kbps %d Hz Při přehrávání z podrobností o položce - Spravovat, odkud by měla být načítána hudba + Spravovat, odkud by měla být načítána hudba Přehrát ze zobrazené položky Zobrazit vlastnosti Vlastnosti skladby @@ -189,11 +181,7 @@ Velikost Přenosová rychlost Vzorkovací frekvence - Složky s hudbou - Obnovit stav přehrávání - Stav obnoven - Obnovit dříve uložený stav přehrávání (pokud existuje) - Nepodařilo se obnovit stav + Složky s hudbou Monitorování hudební knihovny Automatické znovunačítání Sledování změn v hudební knihovně… @@ -218,9 +206,6 @@ Remixové album Živé EP Remixové EP - Vymazat stav přehrávání - Vymazat dříve uložený stav přehrávání (pokud existuje) - Stav vymazán Otevřít frontu Žánr Vlastní akce lišty přehrávání @@ -243,19 +228,17 @@ Vyloučit nehudební obsah Ignorovat zvukové soubory, které nejsou hudbou, například podcasty Obaly alb - Vysoká kvalita + Vysoká kvalita Vypnuté - Rychlé + Rychlé Skrýt spoluautory Zobrazit pouze umělce, kteří jsou přímo uvedeni na albu (funguje nejlépe u dobře označených knihoven) - Nepodařilo se uložit stav %d umělec %d umělci %d umělců %d umělců - Nepodařilo se vymazat stav Znovu najít hudbu Vymazat mezipaměť značek a znovu úplně znovu načíst hudební knihovnu (pomalejší, ale úplnější) Vybráno %d @@ -263,8 +246,8 @@ Wiki %1$s, %2$s Obnovit - Složky - ReplayGain + Složky + Normalizace hlasitosti Chování Změnit motiv a barvy aplikace Přizpůsobit ovládání a chování rozhraní @@ -274,14 +257,12 @@ Nastavit chování zvuku a přehrávání Přehrávání Knihovna - Perzistence Sestupně Seznamy skladeb Obrázek seznamu skladeb pro %s Seznam skladeb Při řazení ignorovat předložky Ignorovat slova jako „the“ při řazení podle názvu (funguje nejlépe u hudby v angličtině) - Vytvořit nový playlist Přidat do seznamu skladeb Přidáno do seznamu skladeb Seznam skladeb vytvořen @@ -339,4 +320,25 @@ Přispějte na projekt a uvidíte zde své jméno! Zůstat ve stavu přehrávání/pozastavení při přeskakování nebo úpravě fronty Zapamatovat pozastavení + Vypnuto + Spustit přehrávání + Spustí Auxio pomocí naposledy uloženého stavu. Pokud není dostupný žádný uložený stav, budou náhodně přehrány všechny skladby. Přehrávání se spustí ihned. +\n +\nVAROVÁNÍ: při ovládání této služby buďte opatrní, pokud ji zavřete a pak se ji znovu pokusíte použít, aplikace nejspíše spadne. + Více + Zpětná vazba + Vytvořit problém na GitHubu + Poslat e-mail + Vybrat složky + Neznámé album + MPEG-4 obsahující %s + Apple Lossless Audio Codec (ALAC) + Neznámý + Nová složka + Úspora místa + Vaše skladby se zobrazí zde. + Vaše alba se zobrazí zde. + Vaši umělci se zobrazí zde. + Vaše seznamy skladeb se zobrazí zde. + Vaše žánry se zobrazí zde. \ No newline at end of file diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml new file mode 100644 index 000000000..c2f6fb9f0 --- /dev/null +++ b/app/src/main/res/values-cy/strings.xml @@ -0,0 +1,332 @@ + + + Yn llwytho cerddoriaeth + Llwytho cerddoriaeth + Yn monitro llyfrgell cerddoriaeth + Ceisio eto + Caniatáu + Caneuon + Cân + Holl ganeuon + Albymau + Albwm + Albwm byw + Albwm ail-gymysgiad + EPau + EP + EP byw + Detholiadau + Detholiad + Detholiad byw + Detholiad ail-gymysgiad + Traciau sain + Trac sain + Tapiau cymysgiad + Tâp cymysgiad + Chwaraewr cerddoriaeth syml a synhwyrol. + Mwy + EP ail-gymysgiad + Enw + Dyddiad + Hyd + Nifer gân + Disg + Trac + Didoli + Didoli gan + Ffolderi + Rhestri chwarae + Rhestr chwarae newydd + Rhestr chwarae gwag + Popeth + Dyddiad wedi\'i ychwanegu + Priodweddau cân + Fformat + Maint + Llawn + Cymharol + Ystadegau llyfrgell + Dewisiad + Manylion y gwall + Cadwyd + Adrodd + Awdur + Cyfrannu + Cyfranwyr + Gweld a rheoli chwarae cerddoriaeth + Yn llwytho eich llyfrgell cerddoriaeth… + Yn monitro eich llyfrgell cerddoriaeth am newidiadau… + Wedi\'i ychwanegu i\'r ciw + Rhestr chwarae wedi\'i greu + Rhestr chwarae wedi\'i fewnforio + Rhestr chwarae wedi\'i ailenwi + Rhestr chwarae wedi\'i allforio + Rhestr chwarae wedi\'i ddileu + Wedi\'i ychwanegu i\'r rhestr chwarae + Cyfrannwch i\'r prosiect er mwyn ychwanegu eich enw yma! + Chwilio eich llyfrgell… + Newid amlygrwydd a threfn tabiau llyfrgell + Wrth chwarae o fanylion yr eitem + Chwarae o\'r eitem wedi\'i dangos + Chwarae o holl ganeuon + Chwarae o\'r albwm + Chwarae o\'r artist + Chwarae o\'r genre + Chwarae\'r gân ar ei phen ei hunan + Cynnwys + Cerddoriaeth + Ail-lwytho awtomatig + Cuddio cyfranwyr + Lluniau + Cloriau albwm + I ffwrdd + Yn gyflym + Ansawdd uwch + Gorfodi cloriau albwm sgwâr + Sain + I ffwrdd + Llyfrgell + Ffolderi cerddoriaeth + Adnewyddu cerddoriaeth + Trac %d + Chwarae pob cân ar hap + Artist anhysbys + Genre anhysbys + Dim dyddiad + Dim disg + Dim trac + Dim caneuon + Dim albymau + Dim cerddoriaeth yn chwarae + Coch + Pinc + Porffor + Porffor dwfn + Glas + Glas dwfn + Gwyrddlas 1 + Gwyrdd + Gwyrdd dwfn + Leim + Melyn + Oren + Llwyd + Brown + Dynamig + %d Wedi\'u dewis + Yn golygu %s + Disg %d + Rhestr chwarae %d + Cymysgiadau DJ + Cymysgiad DJ + Byw + Ail-gymysgiadau + Artistiaid + Rhestr chwarae + Rhestr chwarae wedi\'i mewnforio + Mewnforio + Mewnforio rhestr chwarae + Allforio + Allforio rhestr chwarae + Ailenwi + Ailenwi rhestr chwarae + Dileu + Dileu rhestr chwarae? + Golygu + Chwilio + Hidlo + Disgynnol + Yn chwarae nawr + Gosodiadau sain + Chwarae + Chwarae ar hap + Ciwio + Esgynnol + Chwarae nesaf + Ychwanegu i\'r ciw + Ychwanegu i\'r rhestr chwarae + Mynd i\'r artist + Mynd i\'r albwm + Gweld priodweddau + Gweld + Rhannu + Llwybr + Chwarae ar hap + Chwarae pob cân ar hap + Iawn + Diddymu + Cadw + Ailosod + Ychwanegu + Dull llwybr + Defnyddio llwybrau Windows + Ynghylch + Fersiwn + Cod ffynhonnell + Wici + Trwyddedau + Defnyddio thema ddu pur + Modd crwn + Gosodiadau + Ymddangosiad + Newid thema a lliwiau\'r ap + Thema + Awtomatig + Golau + Tywyll + Lliw + Thema ddu + Galluogi corneli crwn ar elfennau UI ychwanegol (angen cloriau albwm i fod yn grwn) + Personoleiddio + Arddangos + Tabiau\'r llyfrgell + Neidio i\'r nesaf + Modd ailadrodd + Ymarweddiad + Wrth chwarae o\'r llyfrgell + Clawr albwm + Yn llwytho eich llyfrgell cerddoriaeth… (%1$d/%2$d) + Caneuon wedi\'u llwytho: %d + Albymau wedi\'u llwytho: %d + Artistiaid wedi\'u llwytho: %d + Genres wedi\'u llwytho: %d + Cyfanswm hyd: %s + Addasiad heb dagiau + Ffafrio albwm os oes un yn chwarae + Cofio cyflwr oedi + Normaleiddio uchder sain + Strategaeth ReplayGain + Ffafrio trac + Ffafrio albwm + Addasiad gyda thagiau + Ni chanfuwyd cerddoriaeth + Methwyd llwytho cerddoriaeth + Mae ar Auxio angen caniatâd i ddarllen eich llyfrgell gerddoriaeth + Methwyd dod o hyd ap sydd yn gallu gwneud y tasg hon + Dim ffolderi + Neidio i\'r gân nesaf + Neidio i\'r gân ddiwethaf + Newid y modd ail-chwarae + Symud y gân hon + Llun rhestr chwarae %s + Llun y dewis + Sain MPEG-1 + Sain Ogg + %d cilobeit yr eiliad + + %d artistiaid + %d artist + %d artist + %d artist + %d artist + %d artist + + + %d albymau + %d albwm + %d albwm + %d albwm + %d albwm + %d albwm + + Cofio y modd chwarae ar hap + Methwyd llwytho rhestr chwarae o\'r ffeil hon + Methwyd allforio\'r rhestr chwarae i\'r ffeil hon + Symud y tab hwn + Clirio\'r ymholiad chwilio + Tynnu ffolder + Eicon Auxio + Clawr albwm %s + Llun artist %s + Llun genre %s + Chwarae neu seibio + Troi\'r modd chwarae ar hap ymlaen neu i ffwrdd + Stopio\'r chwarae + Tynnu\'r gân hon + Agor y ciw + Sain MPEG-4 + Sain Matroska + Dileu %s? Ni fydd yn bosib dadwneud hyn. + + %d caneuon + %d gân + %d gân + %d cân + %d chân + %d cân + + Senglau + Sengl + Demo + Demos + Ymddengys ar + Artist + Genre + Genres + Didradd + Plws (+) + Chwarae + Indigo + Gwyrddlas 2 + +%.1f desibelau + -%.1f desibelau + %d Hz + Sengl byw + Sengl ail-gymysgiad + Cyfeiriad + Gradd samplu + Alexander Capehart + Allgáu pethau sydd ddim yn gerddoriaeth + Seibio os yn ailadrodd + Seibio pan fydd cân yn ailadrodd + pre-amp ReplayGain + Coma (,) + Gwahannod (;) + Blaenslaes (/) + Didoli\'n ddeallus + Didoli enwau sydd yn dechrau gyda rhifau neu enwau megis \"the\" yn gywir (mae\'n gweithio orau gyda Saesneg) + Chwarae\'n awtomatig gyda chlustffon + Ail-sganio cerddoriaeth + %1$s, %2$s + Rheoli o ble i lwytho cerddoriaeth + Free Lossless Audio Codec (FLAC) + Addasu rheolaethau\'r UI ac ymarweddiad + Gweithred addasedig ar y bar chwarae + Gweithred hysbysiad addasedig + Gwahanydd aml-werth + Tocio pob clawr albwm i gymhareb agwedd 1:1 + Rhybudd: Gall newid y pre-amp i lefel bositif uchel yn achosi uchafbwyntiau ar rai traciau sain. + Ail-lwytho\'r llyfrgell cerddoriaeth, gan ddefnyddio tagiau wedi\'u cadw\'n barod pan fo modd + Dileu\'r tagiau wedi\'u cadw\'n barod ac ail-lwytho\'r llyfrgell gerddoriaeth yn llawn (yn arafach, ond yn fwy cyflawn) + Ni chefnogir y ffolder hon + Advanced Audio Coding (AAC) + Addasu Albwm ReplayGain + Addasu Trac ReplayGain + Mynd i ddechrau\'r gân cyn neidio\'n ôl + Mynd i ddechrau\'r gân cyn neidio i\'r gân flaenorol + Parhau i chwarae / oedi wrth neidio neu wrth golygu\'r ciw + Dechrau chwarae + Diystyru ffeiliau sain sydd ddim yn gerddoriaeth, megis podlediadau + Rhybudd: Gall ddefnyddio\'r gosodiad hwn achosi i rai tagiau i gael eu dehongli\'n anghywir fel bod â gwerthoedd lluosog. Gallwch ddatrys hyn trwy ragddodi llythrennau gwahanydd diangen ag adlach (\\). + Dangos dim ond artistiaid sy\'n cael eu cydnabyddiaeth ar albwm (mae\'n gweithio gorau ar llyfrgelloedd gyda thagiau da) + Dechrau chwarae bob amser pan fydd clustffon wedi\'i gysylltu (efallai na fydd yn gweithio gyda phob un dyfeisiau) + Mae\'r pre-amp yn cael ei gymhwyso i\'r addasiad presennol yn ystod chwarae + Yn dechrau Auxio yn y cyflwr wedi\'i gadw\'n barod. Os nad yw cyflwr sydd wedi\'i gadw ar gael, bydd pob cân yn cael eu chwarae ar hap. Bydd chwarae yn dechrau\'n syth. +\n +\nRHYBUDD: Byddwch yn ofalus wrth reoli\'r gwasanaeth hwn. Os ydych yn ei gau ac yna\'n ceisio ei ddefnyddio, mae\'n debygol y byddwch yn chwalu\'r ap. + Cadw y modd chwarae ar hap wrth chwarae cân newydd + Rheoli sut mae cerddoriaeth a lluniau\'n cael eu llwytho + Ail-lwytho\'r llyfrgell cerddoriaeth pryd bynnag y bydd yn ei newid (mae angen hysbysiad di-ball) + Ffurfweddu llythrennau sydd yn dynodi mwy nag un gwerth tag + Ampersand (&) + Ffurfweddu sain ac ymarweddiad chwarae + Dewis ffolderi + Adborth + Creu issue ar GitHub + Anfon ebost + Mwy + Albwm anhysbys + Anhysbys + MPEG-4 gan gynnwys %s + Apple Lossless Audio Codec (ALAC) + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4f1f2bee1..6eaa0d219 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,10 +24,9 @@ Der Warteschlange hinzugefügt Zum Künstler gehen Zum Album gehen - Wiedergabezustand gespeichert Hinzufügen Speichern - Keine Ordner + Keine Ordner Über Version Quellcode @@ -62,8 +61,6 @@ Zurückspulen, bevor das Lied zurück geändert wird Zurückspulen, bevor zum vorherigen Lied gewechselt wird Inhalt - Wiedergabezustand speichern - Den aktuellen Wiedergabezustand speichern Musik neu laden Musikbibliothek neu laden, verwendet gecachte Tags wenn möglich @@ -71,7 +68,7 @@ Laden der Musik fehlgeschlagen Auxio benötigt die Berechtigung, um deine Musikbibliothek zu lesen Es konnte keine App gefunden werden, die diese Aufgabe übernehmen kann - Das Verzeichnis wird nicht unterstützt + Das Verzeichnis wird nicht unterstützt Musikbibliothek durchsuchen… @@ -126,7 +123,7 @@ Pausieren, wenn ein Song wiederholt wird Zufällig an- oder ausschalten Lied in der Warteschlange verschieben - Verzechnis entfernen + Verzechnis entfernen Albumcover Keine Musik wird gespielt Bibliotheks-Registerkarten @@ -155,13 +152,8 @@ Warnung: Das Erhöhen der Vorverstärkung zu einem hohen positiven Wert könnte zu einer Übersteuerung bei einigen Audiospuren führen. Wenn ein Lied aus den Elementdetails abgespielt wird Vom dargestellten Element abspielen - Musikordner - Verwalten, von wo die Musik geladen werden soll - Modus - Ausschließen - Musik wird nicht aus den von dir hinzugefügten Ordnern geladen. - Einschließen - Musik wird nur aus den von dir hinzugefügten Ordnern geladen. + Musikordner + Verwalten, von wo die Musik geladen werden soll Kein Titel Ogg-Audio MPEG-4-Audio @@ -188,9 +180,6 @@ Hinzugefügt am Musikbibliothek neu laden, sobald es Änderungen gibt (erfordert persistente Benachrichtigung) Automatisch neuladen - Zustand wiederhergestellt - Den vorher gespeicherten Wiedergabezustand wiederherstellen (wenn verfügbar) - Zustand konnte nicht wiederhergestellt werden EP Mini-Alben Single @@ -199,7 +188,6 @@ Kompilation Soundtrack Soundtracks - Wiedergabezustand wiederherstellen Remix-Album Mixtapes Mixtape @@ -210,10 +198,7 @@ Live-Single Remix-Single Live - Den vorher gespeicherten Wiedergabezustand löschen (wenn vorhanden) - Zustand gelöscht Warteschlange öffnen - Wiedergabezustand löschen Genre Equalizer Angepasste Wiedergabeaktionstaste @@ -237,16 +222,14 @@ Audio-Dateien, die keine Musik sind (wie Podcasts), ignorieren Album-Cover Aus - Schnell - Hohe Qualität + Schnell + Hohe Qualität Mitarbeitende ausblenden Nur Künstler anzeigen, die direkt auf einem Album erwähnt werden (funktioniert am besten mit gut getaggten Bibliotheken) %d Künstler %d Künstler - Zustand konnte nicht gelöscht werden - Zustand konnte nicht gespeichert werden Music neu scannen Tag-Cache leeren und die Musik-Bibliothek vollständig neu laden (langsamer, aber vollständiger) %d ausgewählt @@ -261,18 +244,16 @@ Musik Bilder Bibliothek - Ordner + Ordner Wiedergabe Ton und Wiedergabeverhalten konfigurieren - Persistenz - Lautstärkeanpassung ReplayGain + Lautstärkenormalisierung Absteigend Wiedergabelistenbild für %s Wiedergabeliste Wiedergabelisten - Artikel beim Sortieren ignorieren - Wörter wie „the“ ignorieren (funktioniert am besten mit englischsprachiger Musik) - Neue Wiedergabeliste erstellen + Intelligente Sortierung + Korrekte Sortierung von Namen, die mit Nummern oder Wörtern wie „the“ beginnen (funktioniert am besten mit englischsprachiger Musik) Neue Wiedergabeliste Zur Wiedergabeliste hinzugefügt Zur Wiedergabeliste hinzufügen @@ -320,7 +301,7 @@ Wiedergabeliste importieren Wiedergabeliste exportieren Pfadstil - Abolut + Absolut Relativ Windows-kompatible Pfade verwenden Exportieren @@ -330,4 +311,25 @@ Wiedergabeliste exportiert Pause merken Wiedergabe/Pause beim Springen oder Bearbeiten der Warteschlange beibehalten + Aus + Wiedergabe starten + Startet Auxio mit dem vorher gespeicherten Zustand. Wenn kein gespeicherter Zustand vorhanden ist, werden alle Lieder zufällig abgespielt. Die Wiedergabe startet sofort. +\n +\nACHTUNG: Sei vorsichtig bei der Steuerung dieses Dienstes. Wenn du ihn schließt und dann erneut versuchst, ihn zu nutzen, stürzt die App wahrscheinlich ab. + Mehr + Ein Issue auf GitHub erstellen + Rückmeldung + Eine E-Mail senden + Ordner auswählen + MPEG-4, enthält %s + Apple Lossless Audio Codec (ALAC) + Unbekannt + Unbekanntes Album + Neuer Ordner + Speicherplatz sparen + Deine Alben werden hier angezeigt. + Deine Wiedergabelisten werden hier angezeigt. + Deine Lieder werden hier angezeigt. + Deine Genres werden hier angezeigt. + Deine Künstler werden hier angezeigt. \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 039c864ec..6f379565b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -69,7 +69,7 @@ Φόρτωση της συλλογής μουσικής σας… (%1$d/%2$d) Είδη που φορτώθηκαν: %d Αριθμός τραγουδιών - Αυτός ο φάκελος δεν υποστηρίζεται + Αυτός ο φάκελος δεν υποστηρίζεται Δίσκος %d Album που φορτώθηκαν: %d Καλλιτέχνες που φορτώθηκαν: %d @@ -95,7 +95,7 @@ Όνομα Διάρκεια Συνολική διάρκεια: %s - Καθόλου φάκελοι + Καθόλου φάκελοι Μια απλή, λογική εφαρμογή αναπαραγωγής μουσικής για Android. Φόρτωση μουσικής Προβολή και έλεγχος αναπαραγωγής μουσικής @@ -118,18 +118,10 @@ Pυθμός δειγματοληψίας Φόρτωση της μουσικής συλλογής σας… Παρακολούθηση της μουσικής συλλογής σας - Η κατάσταση αναπαραγωγής αποθηκεύτηκε - Εκκαθάριση κατάστασης αναπαραγωγής - Εκκαθάριση της προηγούμενης αποθηκευμένης κατάστασης αναπαραγωγής (αν υπάρχει) - Η κατάσταση αναπαραγωγής εκκαθαρίστηκε - Η κατάσταση αναπαραγωγής επαναφέρθηκε - Αποθήκευση κατάστασης αναπαραγωγής - Αποθήκευση της τωρινής κατάστασης αναπαραγωγής τώρα - Αποκατάσταση κατάσταση αναπαραγωγής Επαναφόρτωση μουσικής Σάουντρακ Ζωντανά - Φάκελοι μουσικής + Φάκελοι μουσικής Μουσικο κομματι Σύνθεση ζωντανών κομματιών Σύνθεση ρεμίξ diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 924e8f202..99573f8c6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -32,10 +32,9 @@ Agregado a la cola Ir al artista Ir al álbum - Estado guardado Agregar Guardar - Sin carpetas + Sin carpetas Acerca de Versión Código fuente @@ -71,8 +70,6 @@ Pausar al repetir Pausar cuando se repite una canción Contenido - Guardar estado de reproducción - Guardar el estado de reproducción ahora Actualizar música Recargar la biblioteca musical, utilizando las etiquetas en caché cuando sea posible @@ -80,7 +77,7 @@ Falló la carga de música Auxio necesita permiso para leer su biblioteca de música No se encontró ninguna aplicación que pueda manejar esta tarea - Directorio no soportado + Directorio no soportado Buscar en la biblioteca… @@ -95,7 +92,7 @@ Mover canción en la cola Mover pestaña Borrar búsqueda - Quitar carpeta + Quitar carpeta Icono de Auxio Carátula de álbum Carátula de álbum para %s @@ -150,10 +147,6 @@ Ajuste sin etiquetas Advertencia: Cambiar el pre-amp a un valor alto puede resultar en picos en algunas pistas de audio. Reproducir desde el elemento que se muestra - Modo - Excluir - La músicano se cargará de las carpetas que añadas. - Incluir Audio matroska Free Losless Audio Codec (FLAC) Advanced Audio Coding (AAC) @@ -162,10 +155,8 @@ Géneros cargados: %d Carga de música Número de canciones - Estado restaurado Recarga automática Recargar la biblioteca musical cada vez que cambie (requiere una notificación persistente) - No es posible restaurar el estado Cargando tu librería de música… Cargando música Monitorizando la librería de música @@ -177,11 +168,8 @@ Frecuencia de muestreo Cancelar Reproducción automática con auriculares - Restablecer el estado de reproducción - Restablecer el estado de reproducción guardado previamente (si existe) - Carpetas de música - Gestionar de dónde se cargará la música - La música solo se cargará de las carpetas que añadas. + Carpetas de música + Gestionar de dónde se cargará la música Dinámico Disco %d Reproducción extendidas (EPs) @@ -214,11 +202,8 @@ Single remix Compilaciones EP de remixes - Eliminar el estado de reproducción guardado previamente (si existe) Abrir la cola Género - Estado limpiado - Limpiar el estado de reproducción Separadores de varios valores Excluye los archivos que no sean música Configurar caracteres que denotan múltiples valores de la etiqueta @@ -238,8 +223,8 @@ Detener la reproducción Ignorar archivos de audio que no sean música, como podcasts Advertencia: El uso de esta configuración puede dar lugar a que algunas etiquetas se interpreten incorrectamente como si tuvieran varios valores. Puede resolverlo anteponiendo a los caracteres separadores no deseados una barra invertida (\\). - Alta calidad - Rápido + Alta calidad + Rápido Acción personalizada de la barra de reproducción Saltar al siguiente Mostrar solo artistas que estén acreditados directamente en un álbum (funciona mejor en bibliotecas bien etiquetadas) @@ -249,8 +234,6 @@ %d artistas %d artistas - No se pudo guardar el estado - No se puede borrar el estado Borrar la caché de las etiquetas y recargar completamente la biblioteca musical (más lento, pero más completo) Volver a escanear la música %d seleccionado @@ -259,14 +242,13 @@ %1$s, %2$s Restablecer Comportamiento - Ganancia de la reproducción + Normalización del volumen Controla cómo se cargan la música y las imágenes Música Imágenes Configurar el comportamiento del sonido y la reproducción Reproducción - Carpetas - Persistencia + Carpetas Cambiar el tema y los colores de la aplicación Personalizar los controles y el comportamiento de la interfaz de usuario Biblioteca @@ -276,7 +258,6 @@ Lista de reproducción Ignorar artículos al ordenar Ignorar palabras como \"the\" al ordenar por nombre (funciona mejor con música en inglés) - Crear una nueva lista de reproducción Nueva lista de reproducción Lista de reproducción %d Agregar a la lista de reproducción @@ -334,4 +315,25 @@ ¡Haga una donación al proyecto para que agreguen su nombre aquí! Recordar la pausa Permanecer en reproducción/pausa al saltar o editar la cola - \ No newline at end of file + Apagar + Iniciar reproducción + Inicia Auxio utilizando el estado previamente guardado. Si no hay ningún estado guardado, todas las canciones se reproducirán aleatoriamente. La reproducción comenzará inmediatamente. +\n +\nADVERTENCIA: Ten cuidado al controlar este servicio, si lo cierras y luego intentas usarlo de nuevo, probablemente se bloqueará la aplicación. + Más + Crear una incidencia en GitHub + Enviar un correo electrónico + Retroalimentación + Seleccionar carpetas + Álbum desconocido + Desconocido + El MPEG-4 contiene %s + Apple Lossless Audio Codec (ALAC) + Tus álbumes aparecerán aquí. + Tus artistas aparecerán aquí. + Tus géneros aparecerán aquí. + Nueva carpeta + Tus listas de reproducción aparecerán aquí. + Tus canciones aparecerán aquí. + Ahorra espacio + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml new file mode 100644 index 000000000..e6117970d --- /dev/null +++ b/app/src/main/res/values-et/strings.xml @@ -0,0 +1,327 @@ + + + Muusika on laadimisel + Jälgime muudatusi muusikakogus + Veel + Anna õigused + Pala + Albumid + Album elavas esituses muusikaga + EP + EP elavas esituses muusikaga + EP remiksidega + Singel + Singel elavas esituses muusikaga + Singel remiksidega + Kogumikud + Kogumik elavas esituses muusikaga + Kogumik remiksidega + Filmimuusika palad + Filmimuusika + Mixtape-kogumik + Mixtape + Demo + Elavas esituses + Esineb + Esitajaid + Žanr + Esitusloendid + Uus esitusloend + Imporditud esitusloend + Impordi esitusloend + Muuda nime + Kustuta + Otsi + Kõik + Nimi + Kuupäev + Palade arv + Plaat + Rada + Lisamise kuupäev + Suund + Kasvavalt + Esita järgmisena + Lisa esitusjärjekorda + Mine esitaja juurde + Mine albumi juurde + Pala teave + Asukoht + Vorming + Bitikiirus + Diskreetimissagedus + Lookohane esitusvaljuse tundlikkus + Albumikohane esitusvaljuse tundlikkus + Sega lood + Sega kõik lood + Alusta taasesitust + Katkesta + Salvesta + Lisa + Asukoha kuvamise viis + Absoluutne + Rakenduse teave + Versioon + Viki + Litsentsid + Muusikakogu statistika + Valik + Veateave + Kopeeritud lõikelauale + Teata veast + Autor + Arenduse eestvedaja Alexander Capehart + Toeta arendajat + Toetajad + Laadime sinu muusikakogu… + Jälgime muudatusi sinu muusikakogus… + Esitusloend on loodud + Esitusloend on imporditud + Esitusloendi nimi on muudetud + Esitusloend on eksporditud + Esitusloend on kustutatud + Lisasime esitusloendisse + Seadistused + Välimus ja tunnetus + Muuda rakenduse kujundust ja värve + Kujundus + Automaatne kujundus + Ümarad nurgad + Kasutajaliidese elementide puhul pruugi ümaraid nurki (eeldab, et ka kaanepiltide nurgad on ümarad) + Laadime muusikat + Lihtne ja ratsionaalne muusikamängija Androidi jaoks. + Album + Proovi uuesti + Palad + Kõik palad + Album remiksidega + EP-albumid + Singlid + Kogumik + Demo-kogumik + DJ-miksid + DJ-miks + Remiksid + Esitaja + Ekspordi esitusloend + Žanrid + Esitusloend + Tühi esitusloend + Muuda esitusloendi nime + Impordi + Ekspordi + Kas kustutame esitusloendi? + Muuda + Filtreeri + Kestus + Järjesta + Sega lood + Järjestuse alus + Kahanevalt + Hetkel esitamisel + Esita + Esitusjärjekord + Vaata teavet + Ekvalaiser + Lisa esitusloendisse + Vaata + Jaga + Suurus + Sobib + Suhteline + Kasuta Windowsiga ühilduvaid asukohti + Lähtekood + Vaata ja halda muusika esitamist + Lähtesta + Lisatud esitusjärjekorda + Kui soovid siin näha oma nime, siis toeta rahaliselt meie arendust! + Otsi oma muusikakogust… + Hele kujundus + Must kujundus + Tume kujundus + Tumeda kujunduse puhul kasuta päris musta kujundust + Värviteema + Purpurpunane + Sügav purpurpunane + Kollane + Rohekassinine + %d. esitusloend + Sinine + Sügavsinine + Free Lossless Audio Codec (FLAC) + Sinakasroheline + %d kbps + Punane + Indigosinine + Roheline + Sügavroheline + Pruun + Roosa + Hall + Laimiroheline + Oranž + Dünaamiline värv + %1$s, %2$s + -%.1f dB + %d valitud + Muudame esitusloendit %s + %d. plaat + +%.1f dB + %d Hz + Laadime sinu muusikakogu… (%1$d/%2$d) + Auxio käivitub varem salvestatud olekus. Kui sellist olekut ei leidu, siis segatakse kõikide lugude järjekord ning taasesitus algab koheselt. +\n +\nHOIATUS: Ole selle teenuse kasutamisel hoolikas - kui sa ta sulged ja siis proovid uuesti kasutada, siis rakendus ilmselt jookseb kokku. + Kohanda endale sobivaks + Seadista kasutajaliidese juhtnuppe ning käitumist + Ekraan + Muusikakogu kaardid + Muuda muusikakogu kaartide nähtavust ja järjekorda + Pildid + Kaanepildid + Pole kasutusel + Kuva kõrgekvaliteedilisena + Kuva kiirelt + Näita kaanepildid teravate nurkadega + Heli + Kadreeri kaanepildid 1:1 küljesuhte alusel + Seadista heli ja taasesitust + Taasesitus + Automaatne esitus kõrvaklappidest + Kõrvaklappide ühendamisel alusta koheselt meedia mängimist (ei pruugi toimida kõikide seadmetega) + Kordamisel tee paus + Enne tagasi hüppamist keri tagasi + Enne eelmise palani tagasi hüppamist keri jooksev pala tagasi + Pala kordamisel tee esituse paus + Jäta paus meelde + Esitusjärjekorras vahelejätmisel või selle muutmisel jää senisesse olekusse (mängimine või paus) + Helivaljuse normaliseerimine + Esitusvaljuse tundlikkuse seadistamise strateegia + Pole kasutusel + Eelista lookohast esitusvaljuse tundlikkust + Eelista albumikohast esitusvaljuse tundlikkust + Kui album juba on esitamisel, siis eelista albumikohast esitusvaljuse tundlikkust + Esitusvaljuse tundlikkuse eelvõimendus + Kohandatud toimingud teavitusel + Kohandatud toimingud taasesitusribal + Esitamisel muusikakogust + Esita näidatud palast + Esita žanrist + Halda muusika ja piltide laadimist + Muusika + Muusikakogu automaatne uuendamine + Välista failid, kus pole muusikat + Järjesta muusikat nii, et arvesse ei lähe numbrid ja artiklid nagu „the“ (toimib kõige paremini ingliskeelse muusika puhul) + Peida kaasautorid + Hüppa järgmise juurde + Taasesituse viis + Esita albumist + Käitumine + Esitamisel pala detailsest vaatest + Esita kõikidest paladest + Esita esitajast + Esita samast palast + Jäta meelde segatud loend + Uue pala esitamisel jäta meelde segatud loendi olek + Sisu + Eira selliseid helifaile, kus pole muusikat (näiteks taskuhäälingud) + Uuenda muusikakogu alati, kui seal midagi muutub (eeldab püsiteavituse olemasolu) + Mitme väärtuse eraldajad siltides + Koma (,) + Semikoolon (;) + Pluss (+) + Ampersand (&) + Seadista tähemärke, mis eraldavad siltides mitut väärtust + Kaldkriips (/) + Hoiatus: selle seadistuse kasutamisel ei pruugi mitu väärtust siltides olla alati korralikult tuvastatud; seda olukorda saad proovida lahendada täiendava prefiksi lisamisega kurakaldkriipsu näol (\\). + Nutikas järjestamine + Näita vaid esitajaid, kes on otsesõnu nimetatud albumi autoriteks (eeldab, et muusikakogu on korralikult sildistatud) + Eelvõimenduse väärtus lisatakse taasesituse ajal olemasolevale valjuse seadistusele + Kohandamine siltide alusel + Kohandamine ilma siltides leiduvate väärtusteta + Hoiatus: kui muudad eelvõimenduse väärtuse suureks positiivseks väärtuseks, siis mõnede lugude puhul võib see tähendada liiga kõrgeid toone. + Muusikakogu + Muusika kaustad + Halda kaustu, kust otsime ja laadime muusikat + Kaustad + Värskenda muusika andmed + Laadi muusikakogu uuesti ning kui võimalik, siis kasuta puhverdatud silte + Laadi muusikakogu uuesti + Kustuta puhverdatud siltide andmed ja laadi muusikakogu tervikuna uuesti (aeglasem, aga täpsem tulemus) + Muusikat ei leidu + Muusika laadimine ei õnnestunud + Auxio vajab muusikakogu töötlemiseks õigust lugeda faile ja kaustu sinu nutiseadmes + Sellest failist ei õnnestu esitusloendit importida + Sellesse faili ei õnnestu esitusloendit eksportida + Ei leidu selle ülesande täitmiseks sobilikku rakendust + Lugu %d + Kaustu pole määratud + See kaust pole toetatud + Esita või peata + Muuda kordamise režiimi + Hüppa järgmise pala juurde + Hüppa viimase pala juurde + Lülita segamisrežiim sisse või välja + Lõpeta taasesitus + Ava esitusjärjekord + Liiguta seda vahelehte + Sega kõik palad + Eemalda see pala + Tõsta see pala teise kohta + Tühjenda otsinguajalugu + Eemalda kaust + Auxio ikoon + Albumi kaanepilt + %s albumi kaanepilt + %s esitaja pilt + %s žanri pilt + %s esitusloendi pilt + Valiku pilt + Tundmatu esitaja + Lugu ei leidu + Tundmatu žanr + Kuupäeva pole + Plaati pole nimetatud + Palasid ei leidu + Albumeid ei leidu + Muusika pole esitamisel + Ogg Vorbis audio + MPEG-1 audio + MPEG-4 audio + Matroska audio + Advanced Audio Coding (AAC) + Laaditud palasid: %d + Kas kustutame %s? Seda tegevust ei saa tagasi pöörata. + + %d pala + %d pala + + Laaditud albumeid: %d + Laaditud esitajaid: %d + Laaditud žanre: %d + Kestus kokku: %s + + %d album + %d albumit + + + %d esitaja + %d esitajat + + Veel + Tagasiside + Koosta GitHubis veateade või ettepanek + Saada e-kiri + Vali kaustad + Tundmatu album + MPEG-4 %s koodekiga + Apple Lossless Audio Codec (ALAC) + Teadmata + Uus kaust + Säästa ruumi + Sinu muusikapalad saavad olema nähtavad siin. + Sinu esitajad saavad olema nähtavad siin. + Sinu žanrid saavad olema nähtavad siin. + Sinu albumid saavad olema nähtavad siin. + Sinu esitusloendid saavad olema nähtavad siin. + \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 63f4df9b6..c10ce2e6c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -52,9 +52,6 @@ Tallenna Palauta oletus Lisää - Tila tallennettu - Tila tyhjennetty - Tila palautettu Valvotaa musiikkikirjastoa muutosten varalta… Sekoita Sekoita kaikki @@ -65,7 +62,7 @@ Muuta kirjastovälilehtien näkyvyyttä ja järjestystä Siirry seuraavaan Kertaustila - Kirjastosta toistettaessa + Kun toistetaan kirjastosta Kohteen tiedoista toistettaessa Muista sekoitus Toista kaikista kappaleista @@ -73,44 +70,32 @@ Toista tyylilajista Moniarvoerottimet Ohita äänitiedostot, jotka eivät ole musiikkia, kuten podcastit - Ja-merkki (&) + Ampersand (&) Pilkku (,) Plus (+) Puolipiste (;) Älykäs järjestys Piilota avustajat Pois - Nopea - Korkea laatu + Nopea + Korkea laatu Ääni Määritä äänen ja toiston toimintaa Toisto Suosi kappaletta ReplayGainin esivahvistus Kirjasto - Musiikkikansiot - Määritä mistä musiikki tulee ladata + Musiikkikansiot + Määritä mistä musiikki tulee ladata Läpikäy musiikki uudelleen - Tila - Ohita - Sisällytä - Musiikkia ladataan vain lisäämistäsi kansioista. - Tallenna toiston tila - Pysyvyys - Tyhjennä toiston tila - Palauta toiston tila - Tyhjennä aiemmin tallennettu toiston tila (jos olemassa) Tähän tehtävään kykenevää sovellusta ei löytynyt - Ei kansioita - Tilaa ei voi palauttaa - Tilaa ei voi tyhjentää + Ei kansioita Raita %d Siirry seuraavaan kappaleeseen Muuta kertaustilaa - Luo uusi soittolista Pysäytä toisto Avaa jono - Poista kansio + Poista kansio Auxion kuvake Albumin %s kansi Tyylilajin %s kuva @@ -186,7 +171,7 @@ Musiikkia ei löytynyt Wiki Harmaa - Muuta sovellukse teemaa ja värejä + Muuta sovelluksen teemaa ja värejä Teema Automaattinen Vaalea @@ -207,26 +192,24 @@ %d esittäjää Nousevasti - Toista seuraava + Toista seuraavaksi Siirry esittäjään Sisältö Musiikki Kuvat Albumikannet - ReplayGain + Äänenvoimakkuuden normalisointi Suosi albumia ReplayGain-strategia Automaattinen uudelleenlataus Automaattitoisto kuulokkeilla Aloita aina toisto, kun kuulokkeet yhdistetään (ei välttämättä toimi kaikilla laitteilla) - Tallenna nykyinen toiston tila Siirry viimeiseen kappaleeseen - Kansiot + Kansiot Toista tai keskeytä - Tämä kansio ei ole tuettu + Tämä kansio ei ole tuettu Sekoitus päällä/pois Sekoita kaikki kappaleet - Tilaa ei voi tallentaa Siirry tätä välilehteä Tyhjennä hakuehto Esittäjän %s kuva @@ -236,8 +219,6 @@ Musiikkia ei toisteta Toista esittäjältä Ohita muu kuin musiikki - Palauta aiemmin tallennettu toiston tila (jos olemassa) - Musiikkia ei ladata valitsemistasi kansioista. Suosi albumia, jos sellaista toistetaan Uusi soittolista Soittolista %d @@ -300,4 +281,45 @@ Soittolistan tuonti tästä tiedostosta ei onnistu Soittolistan vienti tähän tiedostoon ei onnistu Valintakuva - \ No newline at end of file + Pois + Mukautettu toistopalkin toiminto + Aloita toisto + Mixtapet + Mixtape + Näytä vain esittäjät, jotka on suoraan osoitettu jonkin albumin tekijäksi (toimii parhaiten hyvin tagatuissa kirjastoissa) + Kelaa ennen takaisinsiirtymistä + Toista kappale itsenään + Varoitus: Tämän asetuksen käyttäminen voi johtaa joidenkin tagien virheelliseen tulkintaan useina arvoina. Voit ratkaista tämän liittämällä kenoviivan (\\) ei-toivottujen erotinmerkkien eteen. + Järjestä nimet, jotka alkavat numerolla tai sanoilla kuten \"the\", oikein (toimii parhaiten englanninkielisellä musiikilla) + Keskeytä uudelleentoistettaessa + Keskeytä kun kappale toistetaan uudelleen + Toista näytetystä kohteesta + Kelaa takaisin ennen edelliseen kappaleeseen siirtymistä + Demo + Demot + Virhetiedot + Pidä sekoitus päällä kun toistetaan uutta kappaletta + Pysy toisto/keskeytystilassa ohittaessa kappaleita tai muokatessa jonoa + Valitse merkit, jotka erottavat tagiarvot toisistaan + Varoitus: Esivahvistuksen asettaminen korkeaan positiiviseen arvoon saattaa johtaa säröilyyn joissain kappaleissa. + Valitse kansiot + Ilmoita viasta GitHubissa + Lähetä sähköpostia + Lisää + Palaute + Käynnistää Auxion käyttämällä aiemmin tallennettua tilaa. Jos tallennettua tilaa ei ole saatavilla, kaikki kappaleet sekoitetaan. Toisto alkaa välittömästi.\n\nVaroitus: Ole varovainen tämän palvelun hallinnassa. Jos suljet sen ja yrität sitten käyttää sitä uudelleen, sovellus todennäköisesti kaatuu. + Säätö ilman tunnisteita + Esivahvistinta käytetään olemassa olevaan säätöön toiston aikana + Säätö tunnisteilla + Tuntematon albumi + Uusi kansio + MPEG-4 sisältäen %s + Tuntematon + Kappaleesi tulevat näkymään tässä. + Albumisi tulevat näkymään tässä. + Esittäjäsi tulevat näkymään tässä. + Soittolistasi tulevat näkymään tässä. + Tyylilajisi tulevat näkymään tässä. + Apple Lossless Audio Codec (ALAC) + Säätä tilaa + diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index b65baaf8f..f7c9d50a7 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -50,8 +50,6 @@ Laki Tulin ng mga bit Tulin ng sample - Naibalik ang kalagayan - Na-save ang kalagayan Haluin Idagdag I-save diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 839ea4dce..bff206f2b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -72,7 +72,6 @@ %d albums Format - État sauvegardé Mélanger OK Statistiques de la bibliothèque @@ -111,7 +110,7 @@ Compilation Live Chargement de la musique - Suivre la librairie musicale + Suivre la bibliothèque musicale EP EP Singles @@ -132,14 +131,14 @@ Genre inconnu Dynamique Cyan - Aucun dossier - Supprimer le dossier + Aucun dossier + Supprimer le dossier Artiste inconnu Compilation en direct Compilation de remix Mix DJ Mix DJ - Ce dossier n\'est pas pris en charge + Ce dossier n\'est pas pris en charge Réinitialiser Ogg audio Violet foncé @@ -149,12 +148,10 @@ Audio MPEG-4 Pas de date Couverture de l\'album pour %s - État effacé Surveillance de votre bibliothèque musicale pour les changements… Couvertures arrondies Activer les coins arrondis sur des éléments d\'interface utilisateur supplémentaires (nécessite que les couvertures d\'album soient arrondies) Descendant - Etat restauré Personnaliser les commandes et le comportement de l\'interface utilisateur Passer au suivant Mode répétition @@ -172,13 +169,13 @@ Contrôler le chargement de la musique et des images Musique Images - Qualité améliorée (chargement lent) + Qualité améliorée (chargement lent) Configurer les caractères qui indiquent plusieurs valeurs de balise Pochettes d\'albums Masquer les collaborateurs Afficher uniquement les artistes qui sont directement crédités sur un album (fonctionne mieux sur les bibliothèques bien étiquetées) Désactivé - Couvertures originales (téléchargement rapide) + Couvertures originales (téléchargement rapide) Configurer le son et le comportement de lecture Listes de lecture Lors de la lecture depuis la bibliothèque @@ -195,50 +192,36 @@ Lire depuis l\'album Barre oblique (/) Plus (+) - Vider l\'état de lecture précédemment enregistré (s\'il existe) Ajustement avec étiquettes - Dossiers de musique - Gérer d\'où la musique doit être chargée + Dossiers de musique + Gérer d\'où la musique doit être chargée Lecture - Persistance - Vider l\'état de lecture Toujours commencer la lecture lorsqu\'un périphérique audio est connecté (pourrait ne pas fonctionner sur tous les appareils) Stratégie de normalisation de volume Par chanson Par album - Dossiers + Dossiers Par album si un album est en lecture Bibliothèque - La musique sera uniquement chargée des dossiers ajoutés. - Inclure Actualiser la musique Effacer le cache des étiquettes et recharger entièrement la bibliothèque musicale (lent, mais plus complet) Aucune application trouvée qui puisse gérer cette tâche - Impossible de restaurer l\'état - Rétablir l\'état de lecture Auxio a besoin de permissions pour lire votre bibliothèque musicale Tri intelligent Ignorer les nombres ou certains mots comme \"the\" en début de nom lors du tri (fonctionne au mieux avec de la musique en anglais) - Les dossiers de musique ajoutés ne seront pas chargés. Scanner à nouveau la musique Ajustement sans étiquettes - Enregistrer l\'état de lecture actuel maintenant - Rétablir l\'état de lecture enregistré précédemment (s\'il existe) Volume normalisé Le préampli est appliqué à l\'ajustement actuel durant la lecture - Enregistrer l\'état de lecture Lecture automatique avec casque audio Normalisation de volume par préampli Recharger la bibliothèque musicale en utilisant si possible les étiquettes en cache - Mode - Exclure Nouvelle liste de lecture Passer à la chanson suivante Activer ou désactiver la lecture aléatoire %d Hz Passer à la dernière chanson Ajouter à la liste de lecture - Créer une nouvelle liste de lecture Audio Matroska Artistes chargés : %d Rembobiner avant de revenir en arrière @@ -249,7 +232,6 @@ Pause quand une chanson se répète Déplacer cet onglet Renommer - Impossible d\'effacer l\'état Modifier le mode de répétition Albums chargés : %d Durée totale : %s @@ -273,17 +255,16 @@ %d ko/s +%.1f dB -%.1f dB - Supprimer %s&nbsp;\? Cette opération ne peut pas être annulée. + Supprimer %s ? Cette opération ne peut pas être annulée. Avertissement&nbsp;: Le fait de régler le préamplificateur sur une valeur positive élevée peut entraîner l\'apparition des distortions sur certaines pistes audio. Liste de lecture %d Apparaît sur Renommer la liste de lecture - Supprimer la liste de lecture&nbsp;\? + Supprimer la liste de lecture ? Partager Retirer cette chanson Déplacer cette chanson Ouvrir la file d\'attente - Impossible de sauvegarder l\'état Aucune chanson Modification de %s Genres chargés : %d @@ -332,4 +313,12 @@ Absolu Relatif Utiliser les chemins compatibles Windows - \ No newline at end of file + Désactivé + Démarrer la lecture + Démarre Auxio en utilisant le dernier état enregistré. S\'il n\'y en a aucun, tous les titres seront remélangés et la lecture commencera immédiatement.\n\nAVERTISSEMENT : Faites attention en contrôlant ce service, si vous le fermez puis essayez de l\'utiliser à nouveau, vous ferez probablement planter l\'appli. + Plus + Avis + Faire un ticket sur GitHub + Envoyer un courriel + Choisir des dossiers + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 50748355f..765e7879d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -60,13 +60,10 @@ Tamaño Tasa de bits De acordo - Estado restablecido Cambiar o tema e as cores da aplicación Cancelar - Estado gardado Buscando a túa biblioteca… Automático - Estado limpado Código fonte Wiki Tema @@ -109,10 +106,9 @@ %d cancións Son - Alta calidade - Cartafois de música - Excluír - Este cartafol non está soportado + Alta calidade + Cartafois de música + Este cartafol non está soportado Reproducir ou pausar Saltar á seguinte canción Monitorizando a biblioteca de música @@ -146,7 +142,7 @@ Imaxes Portadas de álbums Apagado - Rápido + Rápido Configurar o comportamento de son e reprodución Reprodución Reprodución automática con auriculares @@ -159,27 +155,18 @@ Axuste con etiquetas Axuste sen etiquetas Advertencia: O cambio do pre-amp a un valor alto positivo pode resultar en picos en nalgunhas pistas. - A música se cargará dende os cartafois que engadas. Biblioteca - A música non se cargará dende os cartafois que engadas. - Incluír Actualizar música Recargar a biblioteca de música, utilizando as etiquetas na caché cando sexa posible Volver a escanear a música Borrar a caché das etiquetas a recargar completamente a biblioteca de música (máis lento, pero máis completo) - Persistencia - Limpar o estado de reprodución - Eliminar o estado de reprodución anterior (se existe) - Imposible restaurar o estado - Imposible borrar o estado - Imposible gardar o estado Cambiar o modo de repetición Activar ou desactivar a mezcla Mezclar todas as cancións Deter a reprodución Abrir a cola Borrar o historial de busca - Quitar cartafol + Quitar cartafol Icona de Auxio Portada de álbum Portada de álbum para %s @@ -194,7 +181,7 @@ Fallou a carga de música Auxio necesita permiso para leer a túa biblioteca de música Non se atopou ningunha aplicación que poda facer esta tarefa - Sen cartafois + Sen cartafois Audio ogg Advanced Audio Coding (AAC) Free Lossless Audio Codec (FLAC) @@ -238,16 +225,11 @@ Máis (+) Ignorar palabras como \"the\" ao ordenar por nome (funciona mellor con música en inglés) - Modo - Xestionar dende onde se carga a música + Xestionar dende onde se carga a música Pausar cando se repite unha canción - Cartafois - Gardar o estado de reprodución - Gardar o estado actual de reprodución agora - Restablecer o estado de reprodución gardado previamente (se existe) + Cartafois Ningunha pista Saltar á última canción - Restablecer o estado de reprodución Sen música Pista %d Audio Matroska diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f40159028..bba044624 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -98,8 +98,6 @@ %s हटाएँ\? इसे पूर्ववत नहीं किया जा सकता। लोड किए गए गाने: %d अवरोही - स्थिति साफ की गई - स्थिति सहेजी गई लायब्रेरी टैब की दृश्यता और क्रम बदलें संगीत UI नियंत्रण और व्यवहार अनुकूलित करें @@ -175,7 +173,6 @@ दिखाई देता है साझा करें शफल करें - स्थिति बहाल प्लेलिस्ट का नाम बदला गया अलेक्जेंडर कैपहार्ट द्वारा विकसित एकाधिक टैग मानों को निरूपित करने वाले वर्ण कॉन्फ़िगर करें @@ -185,20 +182,19 @@ संपादन %s पलॅस (+) ऐंपरसैंड (&) - मोड एल्बम कवर %s के लिए एल्बम कवर नीला हरा कोई संगीत नहीं बज रहा अंतिम गीत पर जाएँ - रीप्ले गेन + वॉल्यूम नार्मलाईज़ेशन रीप्ले गेन रणनीति पिछले गाने को छोड़ने से पहले रिवाइंड करें %s के लिए शैली छवि उन्नत ऑडियो कोडिंग (AAC) गहरा हरा बैंगनी - संगीत फ़ोल्डर + संगीत फ़ोल्डर दोहराने पर विराम बंद सभी एल्बम को 1: 1 पहलू अनुपात में कवर करें @@ -206,17 +202,11 @@ जब कोई गीत दोहराया जाता है तो रुक जाएं रीप्लेगेन प्री-एम्प टैग के साथ समायोजन - फ़ोल्डर - बाहर करें - पर्सिस्टेंस - वर्तमान प्लेबैक स्थिति को अभी सहेजें - पहले से सहेजी गई प्लेबैक स्थिति को पुनर्स्थापित करें (यदि कोई हो) + फ़ोल्डर संगीत लोड करना विफल रहा - यह फ़ोल्डर समर्थित नहीं है - स्थिति पुनर्स्थापित करने में असमर्थ + यह फ़ोल्डर समर्थित नहीं है रिपीट मोड बदलें शफ़ल चालू या बंद करें - एक नई प्लेलिस्ट बनाएं सभी गीत शफ़ल करें प्लेबैक बंद करो इस गीत को इस स्थानांतरित करें @@ -233,9 +223,7 @@ स्लेटी %s के लिए प्लेलिस्ट छवि तिथि नहीं - आपके द्वारा जोड़े गए फ़ोल्डरों से संगीत लोड नहीं किया जाएगा। - संगीत केवल आपके द्वारा जोड़े गए फ़ोल्डरों से लोड किया जाएगा। - प्रबंधित करें कि संगीत कहाँ से लोड किया जाना चाहिए + प्रबंधित करें कि संगीत कहाँ से लोड किया जाना चाहिए %s के लिए कलाकार छवि MPEG-4 ऑडियो छवियां @@ -252,11 +240,9 @@ देखें चेतावनी: इस सेटिंग का उपयोग करने से कुछ टैग को गलत तरीके से एकाधिक मान के रूप में व्याख्या किया जा सकता है. आप बैकस्लैश (\\) के साथ अवांछित विभाजक वर्णों को उपसर्ग करके इसे हल कर सकते हैं। केवल उन कलाकारों को दिखाएँ जिन्हें सीधे एल्बम पर श्रेय दिया जाता है (अच्छी तरह से टैग की गई लाइब्रेरी पर अच्छा काम करता है) - तेज - उच्च गुणवत्ता - स्थिति साफ़ करने में असमर्थ - फ़ोल्डर हटाएँ - स्थिती को सहेजने में असमर्थ + तेज + उच्च गुणवत्ता + फ़ोल्डर हटाएँ यह गीत हटाओ डिस्क नहीं निःशुल्क दोषरहित ऑडियो कोडेक (FLAC) @@ -265,9 +251,7 @@ वर्गीकृत एल्बम कवर फोर्स करें ध्वनि और प्लेबैक व्यवहार कॉन्फ़िगर करें चेतावनी: प्री-एम्प को उच्च सकारात्मक मान में बदलने से कुछ ऑडियो ट्रैक पर आवाज फट सकती है। - शामिल करें जब संभव हो तो कैश्ड टैग का उपयोग करके संगीत लाइब्रेरी को पुनः लोड करें - पहले से सहेजी गई प्लेबैक स्थिति साफ़ करें (यदि कोई हो) कोई ऐप नहीं मिला जो इस कार्य को संभाल सके अज्ञात कलाकार इस टैब को स्थानांतरित करें @@ -278,11 +262,8 @@ ट्रैक को प्राथमिकता दें टैग कैश साफ़ करें और संगीत लाइब्रेरी को पूरी तरह पुनः लोड करें (धीमी, लेकिन अधिक पूर्ण) Auxio को आपकी संगीत लाइब्रेरी पढ़ने के लिए अनुमति की आवश्यकता है - कोई फ़ोल्डर नहीं + कोई फ़ोल्डर नहीं भूरा - प्लेबैक स्थिति सहेजें - प्लेबैक स्थिति साफ़ करें - प्लेबैक स्थिति पुनर्स्थापित करें पीला नींबू रंग हेडसेट कनेक्ट होने पर हमेशा चलाना शुरू करें (सभी उपकरणों पर काम नहीं करेगा) @@ -329,4 +310,9 @@ अपना नाम यहां जुड़वाने के लिए परियोजना में दान करें! विराम याद रखें कतार छोड़ते या संपादित करते समय चलता/रोका रखिए + बंद + प्लेबैक शुरू करें + पहले से सहेजे गए स्टेट का उपयोग करके Auxio को शुरू करता है। यदि कोई सहेजा हुआ स्टेट उपलब्ध नहीं है, तो सभी गाने शफल पर चलेंगे और प्लेबैक तुरंत शुरू हो जाएगा। +\n +\nचेतावनी: इस सेवा को नियंत्रित करते समय सावधान रहें, यदि आप इसे बंद करके फिर से उपयोग करने का प्रयास करते हैं, तो संभवतः ऐप क्रैश हो जाएगा। \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 73c4b3fff..d5cea9257 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -9,14 +9,14 @@ Albumi Album Album uživo - Album remixa - Kompilacije za promidžbu - Kompilacija za promidžbu - Kompilacija za promidžbu uživo - Remiks kompilacije za promidžbu + Album remiksa + Mini-albumi + Mini-album + Mini-album uživo + Mini-album remiksa Singlovi Kompilacije - Kompliacija + Kompilacija Zvučni zapis Muzičke zbirke Muzička zbirka @@ -37,15 +37,13 @@ Trenutačno svira Reproduciraj Promiješaj - Popis pjesama + Redoslijed Reproduciraj sljedeću Svojstva pjesme Format Veličina Brzina prijenosa Frekvencija - Stanje spremljeno - Stanje vraćeno Izmiješaj Izmiješaj sve U redu @@ -56,7 +54,7 @@ Inačica Izvorni kod Licencije - Programer: Alexander Capehart + Alexander Capehart Statistika zbirke Izgled Svjetla @@ -65,18 +63,18 @@ Crna tema Koristi potpuno tamnu temu Prikaz - Pločice zbirke - Promijeni vidljivost i redoslijed pločica zbirke + Kartice zbirke + Promijeni vidljivost i redoslijed kartica zbirke Zaobljena naslovnica Zvuk Slušalice: odmah reproduciraj Uvijek pokreni reprodukciju kada su slušalice povezane (možda neće raditi na svim uređajima) - ReplayGain strategija + Strategija normalizacije glasnoće Preferiraj zvučni zapis Preferiraj album - Ako se reproducira album, preferiraj album - ReplayGain pretpojačalo - Pretpojačalo je tijekom reprodukcije primijenjeno postojećoj prilagodbi + Preferiraj album ako se jedan reproducira + Pretpojačalo normalizacije glasnoće + Pretpojačalo se tijekom reprodukcije primijenjue na postojeću podešavanje Prilagođavanje s oznakama Prilagođavanje bez oznaka Upozorenje: Postavljanje pretpojačala na visoke razine može uzrokovati vrhunce tonova u nekim zvučnim zapisima. @@ -85,31 +83,25 @@ Reproduciraj iz svih pjesama Aktualiziraj glazbu Ponovo učitaj glazbenu biblioteku, koristeći predmemorirane oznake kada je to moguće - Mape glazbe - Upravljaj odakle će se glazba učitati - Način - Isključi - Glazba se neće učitati iz dodanih mapa. - Uključi - Glazba će se učitati samo iz dodanih mapa. + Mape glazbe + Upravljaj odakle će se glazba učitati Automatsko ponovno učitavanje Ponovo učitaj svoju zbirku glazbe čim se dogode promjene (zahtijeva stalno obavještavanje) Nijedna glazba nije pronađena Greška u učitvanju glazbe Auxio treba dozvolu za čitanje tvoje zbirke glazbe Nijedna aplikacija ne može obraditi ovaj zadatak - Nema mapa - Ova mapa nije podržana - Nije moguće obnoviti stanje + Nema mapa + Ova mapa nije podržana Pretraži svoju zbirku … Zvučni zapis %d Omogućite ili onemogućite miješanje Izmiješaj sve pjesme Ukoni ovu pjesmu iz popisa pjesama Premjesti ovu pjesmu u popisu pjesama - Pomakni ovu pločicu + Pomakni ovu karticu Izbriši pretražene pojmove - Ukloni mapu + Ukloni mapu Auxio logotip Omot albuma Omot albuma za %s @@ -124,7 +116,7 @@ MPEG-4 zvuk Ogg zvuk Napredno kodiranje zvuka (AAC) - Slobodan audio kodek bez gubitaka (FLAC) + Slobodan kodek zvuka bez gubitaka (FLAC) Crvena Ružičasta Tamnoljubičasta @@ -146,7 +138,7 @@ −%.1f dB %d kbps %d Hz - Učitavanje tvoje zbirke blazbe … (%1$d/%2$d) + Učitavanje tvoje zbirke glazbe … (%1$d/%2$d) Broj učitanih pjesama: %d Broj učitanih albuma: %d Broj učitanih izvođača: %d @@ -174,8 +166,8 @@ Zvučni zapisi Traži Sve - Dodaj u popis pjesama - Dodano u popis pjesama + Dodaj u redoslijed + Dodano u redoslijed izvođenja Pogledaj svojstva Idi na izvođača Idi na album @@ -185,29 +177,22 @@ Automatski Koristi alternativnu radnju za obavijest Omogući zaobljene rubove na dodatnim elementima korisničkog sučelja (zahtijeva zaobljene omote albuma) - Ponašanje + Prilagodi Premotaj prije preskakanja natrag - Spremi trenutno stanje reprodukcije Preskoči na sljedeću pjesmu Reproduciraj iz prikazanog predmeta Zapamti miješanje glazbe - Vrati prethodno spremljeno stanje reprodukcije (ako postoji) Reproduciraj iz albuma Pauziraj pri ponavljanju pjesme Premotaj prije vraćanja na prethodnu pjesmu Reproduciraj ili pauziraj Pauziraj pri ponavljanju Sadržaj - Spremi stanje reprodukcije - Vrati stanje reprodukcije Preskoči na prethodnu pjesmu Promijeni način ponavljanja Ljubičasto - Matroska Zvuk - Izbriši stanje reprodukcije - Stanje izbrisano - Izbriši prethodno stanje reprodukcije (ako postoji) - Otvori popis pjesama + Matroska zvuk + Otvori redoslijed Žanr Zarez (,) Znak i (&) @@ -229,14 +214,12 @@ Isključeno Isključi sve što nije glazba Reproduciraj iz izvođača - Visoka kvaliteta - Brzo + Visoka kvaliteta + Srednja kvaliteta Zanemari sve audio datoteke koje nisu glazba, npr. podcast datoteke Upozorenje: Korištenje ove postavke može dovesti do pogrešnog tumačenja nekih oznaka kao da imaju više vrijednosti. To možeš riješiti postavljanjem obrnute kose crte (\\) ispred neželjenih znakova rastavljanja. Omoti albuma Prikaži samo izvođače koji su izravno navedeni na albumu - Nije moguće očistiti stanje - Nije moguće spremiti stanje %d izvođač %d izvođača @@ -249,25 +232,23 @@ Wiki %1$s, %2$s Resetiraj - ReplayGain - Mape + Normalizacija glasnoće + Mape Silazno Promijenite temu i boje aplikacije Prilagodite kontrole i ponašanje korisničkog sučelja Upravljajte učitavanjem glazbe i slika Slike - Konfigurirajte ponašanje zvuka i reprodukcije + Konfiguriraj ponašanje zvuka i reprodukcije Reprodukcija Fonoteka - Status reprodukcije Popisi pjesama Popis pjesama Glazba Slika popisa pjesama za %s Ponašanje Pametno razvrstavanje - Ispravno razvrstaj imena koja počinju brojevima ili riječima poput „the” (najbolje radi s glazbom na engleskom jeziku) - Stvori novi popis pjesama + Ispravno razvrstaj imena koja počinju s brojevima ili riječima poput „the” (najbolje radi s glazbom na engleskom jeziku) Novi popis pjesama Dodaj u popis pjesama Nema pjesama @@ -283,7 +264,7 @@ Uredi Izbrisati %s\? To je nepovratna radnja. Uređivanje popisa pjesama %s - Sudjelovanja: + Sudjeluje u Dijeli Nema diska Prisili kvadratične omote albuma @@ -297,7 +278,7 @@ Odabir Više Podaci greške - Prijavi grešku + Prijavi Kopirano Nema albuma Uvezen popis pjesama @@ -321,8 +302,29 @@ Koristi Windows kompatibilne putanje Popis pjesama je uvezen Popis pjesama je izvezen - Podešavanje ReplayGain pjesme - Podešavanje ReplayGain albuma + Podešavanje normalizacije glasnoće pjesme + Podešavanje normalizacije glasnoće albuma Zapamti pauzu - Nastavi reprodukciju/pauziranje prilikom preskakanja ili uređivanja slijeda - \ No newline at end of file + Nastavi reprodukciju/pauziranje prilikom preskakanja ili uređivanja redoslijeda + Isključeno + Pokreni reprodukciju + Pokreće Auxio koristeći prethodno spremljeno stanje. Ako nijedno spremljeno stanje nije dostupno, sve će se pjesme nasumično reproducirati. Reprodukcija će započeti odmah. +\n +\nUPOZORENJE: Oprez pri upravljanju ovom uslugom. Ako je zatvoriš i zatim je pokušaš ponovo koristiti, aplikacija će se vjerojatno zatvoriti. + Više + Povratne informacije + Prijavi problem na GitHubu + Pošalji e-mail + Odaberi mape + Appleov kodek zvuka bez gubitaka (ALAC) + Nepoznat album + Nepoznato + MPEG-4 sadrži %s + Niža kvaliteta + Nova mapa + Tvoje pjesme će se ovdje prikazati. + Tvoji albumi će se ovdje prikazati. + Tvoji izvođači će se ovdje prikazati. + Tvoji popisi pjesama će se ovdje prikazati. + Tvoji žanrovi će se ovdje prikazati. + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a994adbda..ea06a90e6 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -9,7 +9,7 @@ Dalok Összes dal Keresés - Szűrő + Szűrés Összes Rendezés Növekvő @@ -45,7 +45,7 @@ Lejátszás/szünet Vörös - Pink + Rózsaszín Bíbor Mély bíbor Indigókék @@ -62,11 +62,11 @@ %d dal - %d dal + %d dalok %d album - %d album + %d albumok EP-k EP @@ -82,8 +82,8 @@ Ismétlő mód módosítása Keverés be/ki kapcsolása %s album borítója - Visszajátszás - Mappa eltávolítása + Hangerő normalizálás + Mappa eltávolítása Lejátszólistához ad Formátum Wiki @@ -95,10 +95,9 @@ Visszajátszás stratégia Inkább album Inkább hangsáv - Ez a mappa nem támogatott + Ez a mappa nem támogatott %s playlista képe Nincs hangsáv - Az aktuális lejátszási állapot mentése most Nincs dal MPEG-1 hang MPEG-4 hang @@ -113,14 +112,13 @@ DJ Mix Műfaj Dal tulajdonságai - Nincs mappa + Nincs mappa Tiszta fekete sötét téma használata Dinamikus Album borítók Visszatekerés az előző dalra való ugrás előtt Figyelem: Az előerősítő magas pozitív értékre módosítása egyes hangsávoknál csúcsosodást eredményezhet. Könyvtár - Állapot Lejátszólista Lejátszólisták Töröl @@ -134,7 +132,6 @@ Új dal lejátszásakor a keverési mód bekapcsolva tartása A számokkal vagy \"the\" típusú szavakkal kezdődő nevek helyes rendezése (legjobban angol nyelvű zenékkel működik) Az összes keverése - A korábban elmentett lejátszási állapot visszaállítása (ha van ilyen) Visszajátszás előerősítő Az előerősítő a lejátszás során a létező beállítással kerül alkalmazásra Helyezze át ezt a dalt @@ -166,35 +163,28 @@ Lejátszólista átnevezés Átnevez Hozzáadás dátuma - Mappák + Mappák Ment Alaphelyzet - Állapot törölve Fejlesztő Alexander Capehart Lejátszás az összes dalból Lejátszás műfajból Tartalom A zenei könyvtár újratöltése, ha változik (állandó értesítést igényel) - Zene könyvtárak - A zene nem töltődik be a hozzáadott mappákból. - Kizárva + Zene könyvtárak A zene betöltése sikertelen Plusz (+) Nem találtunk olyan alkalmazást, amely képes lenne kezelni ezt a feladatot - Állapot helyreállítás nem lehetséges Ismeretlen előadó %1$s, %2$s - Egy új playlista készítése Várósor megnyitás Mozgassa ezt a lapot Keresési lekérdezés törlése Nincs zenelejátszás Egyéni értesítési művelet Nincs dátum - Gyors + Gyors A nem zenei anyagok kizárása - Állapot törlés nem lehetséges - Állapot mentés nem lehetséges Keverés minden dalból Ogg audio Megjelenítés @@ -205,7 +195,6 @@ Fekete téma Lekerekített sarkok engedélyezése további UI elemeken (az albumborítók lekerekítése szükséges) Könyvtár fülek - Mód Free Lossless Audio Codec (FLAC) Beállítás címkékkel A könyvtárból történő lejátszáskor @@ -215,7 +204,6 @@ Zene betöltés Zene betöltése Zene könyvtár figyelése - Állapot mentve Lejátszás megállítása Egyszerű, praktikus zenelejátszó androidra. Matroska hang @@ -239,7 +227,7 @@ Per jel (/) %d előadó - %d előadó + %d előadók Ekvalizer Könyvtár statisztika @@ -257,17 +245,14 @@ Közreműködők elrejtése Csak az albumon közvetlenül feltüntetett előadók megjelenítése (a jól címkézett könyvtárakban működik a legjobban) Ki - Magas minőség + Magas minőség Hang és lejátszási viselkedés konfigurálása Lejátszás Fejhallgató auto. lejátszás Beállítás címkék nélkül - Kezelje, hogy honnan töltsön be zenét + Kezelje, hogy honnan töltsön be zenét Zene újraolvasása A címkék gyorsítótárának törlése és a zenei könyvtár teljes újratöltése (lassabb, de teljesebb) - Lejátszási állapot mentése - Lejátszási állapot törlése - A zene csak az Ön által hozzáadott mappákból töltődik be. A zenei könyvtár újratöltése, lehetőség szerint a gyorstárazott címkék használatával %d kiválasztott %d lemez @@ -281,10 +266,6 @@ Megjelenik itt, Megoszt Lejátszólista törlése\? - Állapot helyreállítva - A korábban elmentett lejátszási állapot törlése (ha van ilyen) - Lejátszási állapot visszaállítása - Tartalmaz Az Auxio engedélyt kér a zenei könyvtár olvasásához Távolítsa el ezt a dalt Auxio ikon @@ -303,4 +284,31 @@ Másolva Jelentés Hiba információ + Ki + Importált lejátszási lista + Lejátszási lista exportálva + Demó + ReplayGain Track módosítása + ReplayGain Album módosítása + Szerző + Adományozás + Támogatók + Adományozzon a projektnek, hogy itt legyen a neved! + Szünet megjegyzése + A lejátszás/szünet megmarad a várólista kihagyásakor vagy szerkesztésekor + Cím + Nincsenek albumok + Lejátszási lista exportálása + Használja a Windows-kompatibilis útvonalakat + Path stílus + Export + Import + Lejátszási lista importálása + Demók + Üres lejátszási lista + Abszolút + Relatív + Lejátszási lista importálva + Nem sikerült importálni a lejátszási listát ebből a fájlból + Nem sikerült exportálni a lejátszási listát ebbe a fájlba \ No newline at end of file diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index 79cd80ab4..693501bfe 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -74,7 +74,6 @@ Reinitialisar Stylo de percurso Usar percursos compatibile con Windows - Stato salveguardate A proposito de Un reproductor de musica simple e rational pro Android. Genere @@ -126,32 +125,26 @@ Imagine de genere ab %s Statisticas del bibliotheca Bibliotheca - Stato radite Copiate Modo de repetition Reproducer ab tote le cantos Contento Musica Audio - Crear un nove lista de reproduction Remover iste canto Copertura de album Coperrturas de album Disactivate - Rapide + Rapide Reproduction Rememorar le pausa - Dossieres de musica - Dossieres - Modo + Dossieres de musica + Dossieres Actualisar le musica - Salveguardar stato de reproduction - Restabilir le stato de reproduction Nulle musica trovate Falleva le carga del musica - Necun dossieres - Iste dossier non es supportate - Non poteva salveguardar le stato + Necun dossieres + Iste dossier non es supportate Tracia %d Reproducer o pausar Saltar al canto sequente @@ -159,7 +152,7 @@ Cambiar modo de repetition Stoppar le reproduction Aperir le cauda - Remover le dossier + Remover le dossier Icone de Auxio Copertura de album pro %s Artista incognite @@ -188,8 +181,6 @@ %d canto %d cantos - Non poteva rader le stato - Non poteva restaurar le stato Cantos cargate: %d Albumes cargate: %d Artistas cargate: %d @@ -202,4 +193,27 @@ Lista de reproduction delite Recargamento automatic Imagines + Cargante tu bibliotheca de musica… + Action personalisate del barra de reproduction + Vader al artista + Plus (+) + Configurar le sono e le comportamento de reproduction + Deler %s? Isto non es pote disfacer. + +%.1f dB + -%.1f dB + %d Hz + %d kbps + Cargante le bibliotheca de musica… (%1$d/%2$d) + Importar lista de reproduction + Action de notification personalisate + Controlar le cargamento del musica e imagines + Adjustamento sin etiquettas + Rescannar le musica + Rader le requestas de recerca + Imagine de artista pro %s + Imagine del lista de reproduction pro %s + Necun musica in reproduction + Modificante %s + Lista de reproduction %d + Initiar le reproduction \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 707c86f24..d0c9761fd 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -43,13 +43,13 @@ Telusuri pustaka Anda… Trek %d - Putar/Jeda + Putar atau jeda - %d Lagu + %d lagu - %d Album + %d album Properti lagu Laju bit @@ -84,7 +84,6 @@ Tindakan notifikasi khusus Acak Acak semua - Status disimpan Utamakan trek Utamakan album Pra-amp ReplayGain @@ -99,26 +98,22 @@ Putar mundur sebelum melompat ke lagu sebelumnya Jeda saat lagu diulang Konten - Simpan status pemutaran - Simpan status pemutaran saat ini sekarang Muat ulang musik - Akan memulai ulang aplikasi - Kelola dari mana musik dimuat - Kecualikan - Termasuk - Tidak ada aplikasi yang bisa membuka tautan ini - Folder ini tidak didukung + Muat ulang perpustakaan musik, menggunakan tag yang di-cache jika memungkinkan + Kelola dari mana musik dimuat + Tidak ada aplikasi yang bisa membuka tugas ini + Folder ini tidak didukung Pindahkan tab ini - Tidak Ada Nomor Trek + Tidak ada nomor trek Audio MPEG-1 Pengkodean Audio Tingkat Lanjut (AAC) Free Lossless Audio Codec (FLAC) Merah Ungu - Deep Purple + Ungu tua Nila Biru - Biru Tua + Biru tua Lime Dinamis Disk %d @@ -134,26 +129,23 @@ Saat diputar dari pustaka Putar dari album Ubah mode pengulangan - Gambar Artis untuk %s + Gambar artis untuk %s Saat diputar dari keterangan item - Musik tidak akan dimuat dari folder yang Anda tambahkan. Hapus lagu antrian ini Hapus kueri pencarian Penyesuaian tanpa tag - Folder musik + Folder musik Putar dari artis - Mode Auxio memerlukan izin untuk membaca perpustakaan musik Anda Loncat ke lagu terakhir Acak semua lagu - Tidak Ada Tanggal - Musik hanya akan dimuat dari folder yang Anda tambahkan. + Tidak ada tanggal Pemuatan musik gagal - Sampul Album untuk %s - Artis Tidak Dikenal - Tidak ada Folder + Sampul album untuk %s + Artis tidak dikenal + Tidak ada folder Loncat ke lagu berikutnya - Hapus direktori + Hapus folder Ikon Auxio Sampul album Aktifkan atau nonaktifkan acak @@ -163,21 +155,21 @@ Cokelat Abu-abu Total durasi: %s - Gambar Genre untuk %s - Genre Tidak Diketahui + Gambar genre untuk %s + Genre tidak diketahui Audio Matroska - Hijau Tua + Hijau tua Kuning Audio MPEG-4 - Cyan - Teal + Sian + Hijau laut Hijau Jingga Genre yang dimuat: %d Jumlah lagu Trek Merah muda - ReplayGain + Normalisasi volume Muat ulang otomatis Selalu muat ulang pustaka musik saat terjadi perubahan (membutuhkan notifikasi tetap) Perilaku @@ -186,16 +178,16 @@ Koma (,) Tambah (+) Tanggal ditambahkan - Kualitas tinggi + Kualitas tinggi Titik koma (;) Wiki Putar dari aliran Aliran Sampul album Nonaktif - Cepat + Cepat Album remix - Folder + Folder Memuat musik Memantau pustaka musik Memantau perubahan pada pustaka musik Anda… @@ -231,4 +223,88 @@ Kompilasi live EP Remix Single + Daftar putar dibuat + Ditambahkan ke daftar putar + Bersihkan cache tag dan muat ulang perpustakaan musik secara penuh (lebih lambat, tetapi lebih sempurna) + Daftar putar yang diimpor + Daftar putar diimpor + Daftar putar diekspor + Daftar putar dihapus + Tampilkan + Kecualikan item non-musik + Paksa sampul album kotak + Potong semua sampul album ke aspek rasio 1:1 + %d terpilih + Ubah nama + Ubah nama daftar putar + Hapus daftar putar? + Gambar daftar putar untuk %s + Demo + Demo + Tambahkan ke daftar putar + Remix + Hentikan pemutaran + Hapus + Urutkan berdasarkan + Turun + Penyesuaian Trek ReplayGain + Penyesuaian Album ReplayGain + Disalin + Laporkan + Tidak ada lagu + Pencipta + Donasi + Pendukung + Donasi ke proyek ini agar nama Anda ditambahkan kesini! + Pemisah nilai ganda + Konfigurasikan suara dan perilaku pemutaran + Ingat jeda + Tetap memainkan/menjeda ketika melewati atau mengatur antrean + Tidak dapat mengekspor daftar putar ke berkas ini + Buka antrean + Pemilihan gambar + Tidak ada disk + Tidak ada album + %1$s, %2$s + Daftar putar baru + Arah + Gunakan jalur yang kompatibel dengan Windows + Langsung + DJ Campuran + DJ Campuran + Lewati ke lagu berikutnya + Mode perulangan + Peringatan: Menggunakan pengaturan ini mungkin akan menghasilkan beberapa tag salah diinterpretasikan sebagai nilai multipel. Anda dapat mengatasinya dengan mengawali karakter yang tidak diinginkan dengan garis miring terbalik (\\). + Nonaktif + Daftar putar kosong + Impor daftar putar + Atur + Ekualiser + Atur ulang + Pindai ulang musik + Bagikan + Daftar putar + Muncul di + Daftar putar + Jalur + Pilihan + Nama daftar putar diubah + Aksi bar playback kustom + Putar lagu dengan sendirinya + Konfigurasi karakter yang menunjukkan nilai tag multipel + Penyortiran cerdas + Hanya tampilkan artis yang secara langsung dikreditkan pada sebuah album (bekerja paling baik pada pustaka yang diberi tag dengan baik) + Sembunyikan kolaborator + Mengedit %s + Daftar putar %d + Hapus %s? Aksi ini tidak dapat dikembalikan. + Tidak dapat mengimpor daftar putar dari berkas ini + Gaya jalur + Absolut + Impor + Ekspor + Ekspor daftar putar + Relatif + Informasi kesalahan + Sortir nama yang dimulai dengan nomor atau kata seperti \"the\" dengan benar (bekerja paling baik dengan lagu berbahasa Inggris) \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 27372f238..a152d00e3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,8 +1,8 @@ - Semplice e razionale lettore musicale per android. - Vedi e gestisci la riproduzione musicale + Un lettore musicale semplice e razionale per android. + Visualizza e gestisci la riproduzione musicale Riprova Autorizza @@ -10,7 +10,7 @@ Artisti Album Brani - Tutte i brani + Tutti i brani Cerca Filtro Tutto @@ -18,90 +18,87 @@ Nome Artista Album - Anno - Ascendente - Ora in riproduzione + Data + Crescente + In riproduzione Riproduci - Mescola + Casuale Riproduci da tutti i brani Riproduci dall\'album Riproduci dall\'artista Coda - Riproduci successivo - Accoda - Accodato + Riproduci come successivo + Aggiungi alla coda + Aggiungi alla coda Vai all\'artista Vai all\'album - Stato salvato Aggiungi Salva - Nessuna cartella + Nessuna cartella Informazioni Versione Codice sorgente Licenze - Sviluppato da Alexander Capehart - Statistiche della raccolta + Alexander Capehart + Statistiche della libreria Opzioni Aspetto Tema - Automatico + Sistema Chiaro Scuro Accento Tema nero - Usa tema nero puro + Usa un tema scuro AMOLED Visualizzazione Schede libreria Cambia la visibilità e l\'ordine delle schede della libreria - Copertine dischi arrotondate - Abilita gli angoli arrotondati su ulteriori elementi dell\'interfaccia utente (richiede che le copertine degli album siano arrotondate) - Usa azioni notifica alternative + Copertine arrotondate + Abilita gli angoli arrotondati su ulteriori elementi dell\'interfaccia (richiede che le copertine degli album siano arrotondate) + Personalizza azione notifica Audio - Autoplay cuffie - Comincia la riproduzione ogni volta che le cuffie sono inserite (potrebbe non funzionare su tutti i dispositivi) - Strategia ReplayGain + Riproduzione automatica + Inizia la riproduzione ogni volta che vengono connesse delle cuffie (potrebbe non funzionare su tutti i dispositivi) + Modalità ReplayGain Preferisci traccia Preferisci album Preferisci l\'album se in riproduzione - Comportamento + Personalizza Quando in riproduzione dalla libreria - Mantieni mescolamento - Mantiene il mescolamento anche se un nuovo brano è selezionato - Riavvolgi prima di saltare indietro - Riavvolge prima di saltare al brano precedente + Ricorda riproduzione casuale + Mantiene la riproduzione casuale quando viene riprodotto un nuovo brano + Riavvolgi prima di tornare indietro + Riavvolge il brano in riproduzione prima di passare a quello precedente Pausa alla ripetizione - Pausa quando un brano si ripete + Mette in pausa quando un brano si ripete Contenuti - Salva stato riproduzione - Salva lo stato di riproduzione corrente - Aggiorna musica + Aggiorna libreria Ricarica la libreria musicale, usando i tag nella cache quando possibile Musica non trovata Caricamento musica fallito Auxio ha bisogno del permesso per leggere la tua libreria musicale Nessuna app può completare questa azione - Questa cartella non è supportata + Questa cartella non è supportata Cerca nella libreria… - Canzone %d - Riproduci o pausa - Salta a brano successivo - Salta a ultimo brano + Traccia %d + Riproduci o metti in pausa + Passa al brano successivo + Passa all\'ultimo brano Cambia modalità ripetizione - Attiva o disattiva mescolamento - Mescola tutti i brani + Attiva o disattiva la riproduzione casuale + Riproduce casualmente tutti i brani Rimuovi questo brano Sposta questo brano - Muove questa scheda - Cancella la query di ricerca - Rimuovi cartella + Sposta questa scheda + Cancella la ricerca + Rimuovi cartella Icona Auxio Copertina album - Copertina album per %s + Copertina album di %s Immagine artista per %s Immagine genere per %s @@ -109,7 +106,7 @@ Genere sconosciuto Data sconosciuta Nessuna traccia - Nessuna canzone in riproduzione + Nessun brano in riproduzione Rosso Rosa @@ -124,7 +121,7 @@ Verde scuro Lime Giallo - Arancio + Arancione Marrone Grigio @@ -143,33 +140,28 @@ %d album %d album - Modo - La musica non sarà caricata dalle cartelle che aggiungi. - Includi - La musica sarà caricata solo dalle cartelle che aggiungi. MPEG-1 audio MPEG-4 audio Ogg audio Dinamico +%.1f dB %d Hz - Caricamento libreria musicale… (%1$d/%2$d) - Quando in riproduzione dai dettagli dell\'elemento - Attenzione: impostare valore positivi alti può provocare distorsioni su alcune tracce. + Caricamento della tua libreria musicale… (%1$d/%2$d) + Quando riproduci dai dettagli di un elemento + Attenzione: impostare alti valori positivi può provocare la distorsione di alcune tracce. Regolazione senza tag - Mescola - Mescola tutto - Regolazione con tag + Casuale + Tutto in casuale + Regolazione mediante tag Il pre-amp è applicato alla regolazione esistente durante la riproduzione Riproduci dall\'elemento mostrato - Gestisci le cartelle da dove caricare la musica - Cartelle musica - Escludi + Gestisci le cartelle da dove caricare la musica + Cartelle musicali Matroska audio Free Lossless Audio Codec (FLAC) Advanced Audio Coding (AAC) Disco %d - %d kB/s + %d kbps -%.1f dB Caricamento musica Caricamento libreria musicale… @@ -179,23 +171,19 @@ Traccia OK Frequenza di campionamento - Vedi proprietà + Visualizza proprietà Proprietà brano Formato Dimensione - Bitrate - Cancella + Velocità in bit + Annulla Pre-amp ReplayGain Sto monitorando i cambiamenti nella tua libreria musicale… Ricarica la tua libreria musicale se subisce cambiamenti (richiede notifica persistente) Caricamento musica Monitoraggio libreria musicale - Stato ripristinato Data aggiunta Ricaricamento automatico - Ripristina stato riproduzione - Ripristina lo stato di riproduzione precedentemente salvato (se disponibile) - Impossibile ripristinare lo stato EP EP Singoli @@ -209,38 +197,35 @@ Singolo live Album live EP live - Raccolta + Compilation Colonne sonore Singolo remix - Raccolte + Compilation Colonna sonora - Ripristina lo stato di riproduzione precedentemente salvato (se presente) - Ripristina stato riproduzione Equalizzatore Apri la coda Genere - Stato ripristinato - Azione personalizzata barra di riproduzione - Vai alla prossima + Personalizza azione barra di riproduzione + Passa al brano successivo Modalità ripetizione - Interrompi riproduzione + Interrompi la riproduzione Nascondi collaboratori - Mostra solo artisti che sono direttamente accreditati in un album (funziona meglio su librerie ben taggate) + Mostra solo gli artisti che appaiono esplicitamente nei riconoscimenti di un album (funziona meglio su librerie ben taggate) Copertine album - Off - Veloce - Escludi file non musicali - Ignora file audio non musicali, come i podcast + Disattiva copertine + Media qualità + Escludi i file non musicali + Ignora i file audio non musicali, come i podcast Separatori multi-valore - Configura caratteri che indicano valori multipli di tag + Configura i caratteri che identificano tag con valori multipli Barra (/) - Attenzione: potrebbero verificarsi degli errori nella interpretazione di alcuni tag con valori multipli. Puoi risolvere aggiungendo come prefisso la barra rovesciata (\\) ai separatori indesiderati. + Attenzione: potrebbero verificarsi degli errori nell\'interpretazione di alcuni tag con valori multipli. Puoi risolvere aggiungendo come prefisso la barra rovesciata () ai separatori indesiderati. E commerciale (&) - Raccolte live - Raccolta remix + Compilation live + Compilation remix Mix DJ Mix DJ - Alta qualità + Alta qualità Virgola (,) Punto e virgola (;) Più (+) @@ -250,37 +235,33 @@ %d artisti Riscansiona musica - Impossibile salvare Svuota la cache dei tag e ricarica completamente la libreria musicale (più lento, ma più completo) - Impossibile svuotare %d selezionati Riproduci dal genere Wiki %1$s, %2$s Ripristina Comportamento - Cartelle + Cartelle Musica Immagini - Collezione + Libreria Cambia il tema e i colori dell\'app Controlla come vengono caricate musica e immagini - ReplayGain + Normalizzazione del volume Riproduzione - Persistenza - Personalizza controlli e comportamento dell\'UI - Configura comportamento di suono e riproduzione - Discendente + Personalizza i controlli e il comportamento dell\'interfaccia + Suono e riproduzione + Decrescente Playlist Playlist - Ordinazione intelligente - Ordina correttamente i nomi che iniziano con numeri o parole come \"the\" (funziona meglio con i titoli in inglese) - Crea una nuova playlist + Ordinamento intelligente + Ordina correttamente i titoli che iniziano con numeri o parole come \"the\" (funziona meglio con i titoli in inglese) Immagine della playlist per %s Nuova playlist - Aggiungi a playlist + Aggiungi alla playlist Playlist creata - Aggiunto a playlist + Aggiunto alla playlist Nessun brano Playlist %d Elimina @@ -293,31 +274,31 @@ Playlist rinominata Condividi Nessun disco - Appare su - Modifica di %s + Appare in + Modificando %s Forza copertine album quadrate Adatta tutte le copertine degli album ad una visualizzazione 1:1 Brano Visualizza - Riproduci brano da solo + Riproduci solo il brano Ordina per - Playlist importata + Playlist esterna Impossibile esportare la playlist in questo file - Direzione + Ordine Espandi - Immagine di selezione + Selezione immagine Selezione Copiato Segnala Autore Dona - Supporti + Sostenitori Informazioni sull\'errore Nessun album Percorso Playlist vuota Importa playlist - Dona al progetto; il tuo nome sarà aggiunto qui! + Dona al progetto, il tuo nome sarà aggiunto qui! Impossibile importare una playlist da questo file Importa Esporta @@ -328,4 +309,29 @@ Usa percorsi compatibili con Windows Playlist importata Playlist esportata + Disattivato + Demo + Demo + Regolazione traccia ReplayGain + Regolazione album ReplayGain + Ricorda pausa + Mantiene riproduzione/pausa durante il cambio di brano e la modifica della coda di riproduzione + Avvia la riproduzione + Avvia Auxio utilizzando le ultime impostazioni di riproduzione salvate. Se non è disponibile alcuna impostazione di riproduzione, i brani verranno riprodotti in ordine casuale. La riproduzione inizierà immediatamente.\n\nAttenzione: fai attenzione a controllare questa impostazione; se lo chiudi e poi provi a usarlo di nuovo, è probabile che l\'app si blocchi. + Scegli le cartelle + Feedback + Invia un\'email + Apri un\'issue su GitHub + Album sconosciuto + I tuoi generi appariranno qui. + I tuoi brani appariranno qui. + I tuoi artisti appariranno qui. + I tuoi album appariranno qui. + Le tue playlist appariranno qui. + Espandi + Nuova cartella + Libera spazio + MPEG-4 contenente %s + Apple Lossless Audio Codec (ALAC) + Sconosciuto \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e39afd69e..8b9ad9827 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -56,7 +56,6 @@ שמירה אתחול הוספה - המצב נשמר גרסה קוד מקור ויקי @@ -107,14 +106,14 @@ הצגת אומנים שמצויינים ישירות בקרדיטים של אלבום בלבד (עובד באופן מיטבי על ספריות מתויגות היטב) עטיפות אלבום כבוי - מהיר + מהיר שמע ניגון ניגון אוטומטי באוזניות הרצה לאחור לפני דילוג אחורה הרצה לאחור לפני דילוג לשיר הקודם עצירה בעת חזרה - ReplayGain + נורמליזציית עוצמת הקול העדפת אלבום מגבר ReplayGain התאמה עם תגיות @@ -132,7 +131,6 @@ תור מעבר לאומן ערבוב - המצב שוחזר אודות הגדרות אוטומטי @@ -145,7 +143,7 @@ התאמת תווים המציינים ערכי תגית מרובים קו נטוי (/) אזהרה: השימוש בהגדרה זו עלול לגרום לחלק מהתגיות להיות מפורשות באופן שגוי כבעלות מספר ערכים. ניתן לפתור זאת על ידי הכנסת קו נטוי אחורי (\\) לפני תווים מפרידים לא רצויים. - איכות גבוהה + איכות גבוהה התעלמות ממספרים או מילים כמו \"The\" (\"ה׳ היידוע\") בעת סידור על פי שם (עובד באופן מיטבי עם מוזיקה בשפה האנגלית) תמונות הגדרת הצליל והניגון @@ -166,13 +164,10 @@ שינוי שם רשימת השמעה למחוק את רשימת ההשמעה\? עריכה - לא ניתן לנקות את המצב כתום - תיקיות מוזיקה + תיקיות מוזיקה טעינה מחדש של ספריית המוזיקה, במידה וניתן ייעשה שימוש בתגיות מהמטמון סריקת מוסיקה מחדש - שמירת מצב הנגינה - לא ניתן לשמור את המצב ‏ Auxio צריך הרשאות על מנת לקרוא את ספריית המוזיקה שלך פתיחת התור משך כולל: %s @@ -181,12 +176,10 @@ שירים טעונים: %d אלבומים טעונים: %d ז\'אנרים טעונים: %d - המצב נוקה ספריה - שמירת מצב הנגינה הנוכחי כעת לא נמצא יישום שיכול לטפל במשימה זו - אין תיקיות - תיקייה זו אינה נתמכת + אין תיקיות + תיקייה זו אינה נתמכת דילוג לשיר האחרון שינוי מצב חזרה ניגון או השהיה @@ -206,22 +199,20 @@ דינמי המוזיקה שלך בטעינה… (‎%1$d/%2$d) דיסק %d - ניהול המקומות שמהם תיטען מוזיקה + ניהול המקומות שמהם תיטען מוזיקה אין שירים ורוד נוצרה רשימת השמעה - תיקיות + תיקיות אומן אחד שני אומנים %d אומנים - לכלול רענון מוזיקה לא נמצאה מוזיקה אירע כשל בטעינה מוזיקה עטיפת אלבום - ניקוי מצב הנגינה שיר אחד שני שירים @@ -237,18 +228,14 @@ נוסף לרשימת השמעה ערבוב כל השירים סמל Auxio - הסרת תיקייה + הסרת תיקייה תמונת רשימת השמעה עבור %s אדום ירוק - לא ניתן לשחזר את המצב רצועה %d - יצירת רשימת השמעה חדשה עצירת הנגינה הסרת שיר זה שיתוף - מצב - החרגה העברת שיר זה העברת לשונית זו ניקוי תור החיפוש @@ -261,16 +248,12 @@ הצגה הכרחת עטיפות אלבום מרובעות ריקון מטמון התגיות וטעינת ספריית המוזיקה מחדש במלואה (איטי יותר, אך יותר שלם) - ניקוי מצב הנגינה הקודם שנשמר (אם קיים) מיון על פי כיוון חיתוך כל עטיפות האלבומים ליחס של 1:1 - מוזיקה לא תיטען מהתיקיות שנוספו. - מוזיקה תיטען רק מהתיקיות שנוספו. מופיע~ה ב- ניגון השיר בלבד אזהרה: שינוי המגבר לערך חיובי גבוה עלול לגרום לעיוות (דיסטורשן) בחלק מרצועות האודיו. - שחזור מצב נגינה אינדיגו אודיו MPEG-1 אודיו MPEG-4 @@ -279,7 +262,6 @@ טורקיז חום %d נבחרו - התמדה עוד בחירה מידע על השגיאה @@ -293,9 +275,8 @@ %d הרץ (Hz) %d קילוביטים לשנייה (kbps) מועתק - שחזור מצב הנגינה שנשמר קודם (אם קיים) אודיו Matroska - קידוד אודיו מתקדם (AAC) + קודק אודיו מתקדם (AAC) %1$s, %2$s ליים %s נערך @@ -317,4 +298,25 @@ רשימת השמעה יוצאה יצא רשימת השמעה אין יכולת לייצא רשימת השמעה מהקובץ הנ”ל + בחר תיקיות + התחל השמעה + זכור השהיה + כבוי + עוד + תרומה + MPEG-4 המכיל %s + לא ידוע + אלבום לא ידוע + תיקון רצועות ReplayGain + תיקון אלבומים ReplayGain + יוצר + תיקיה חדשה + תרמו לפרויקט כדי להוסיף את שמכם כאן! + מפעיל Auxio במצב שנשמר קודם לכן. אם אין מצב שמור זמין, כל השירים יתערבבו. השמעה תתחיל מידית.\n\nאזהרה: היזהר בשליטה בשירות זה, אם תסגרו אותו ואחר כך תנסו להשתמש בו שוב, ייתכן שתקרסו את האפליקציה. + הישאר בניגון/בהשהיה בעת דילוג או עריכה של התור + קודק אודיו Apple ללא אובדן נתונים (ALAC) + משוב + תומכים + יצירת דוח ב-GitHub + שליחת מייל \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index fd4e11299..8004f19ff 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -6,7 +6,6 @@ アーティスト ジャンル 曲の長さ - 現在の再生状態を保存 このタブを移動 この再生待ちの曲を移動 日付けがありません @@ -25,7 +24,7 @@ 曲の繰り返し時にポーズ 検索クエリを解除 日付け - 高クオリティ + 高クオリティ ラウンドモード 音楽が見つかりません 音楽の読み込みに失敗 @@ -41,7 +40,7 @@ ライブラリタブの視度と順序をカスタマイズ アルバムで直接クレジットされたアーティストのみを表示 (適正にタグ付けされたライブラリで最適に作動します) 前の曲にスキップ前に曲を巻き戻す - 音楽フォルダ + 音楽フォルダ プラス (+) リミックスコンピレーション DJミックス @@ -53,7 +52,6 @@ 再生中 カラースキーム 黒基調 - 再生状態を復元 表示されたアイテムから再生 再生停止 @@ -78,19 +76,15 @@ アンパサンド (&) アルバムを優先 トラックを優先 - 音楽の読み込み元を管理 - 除外 + 音楽の読み込み元を管理 音楽の再読み込み - 再生状態を保存 Auxio は音楽ライブラリを読む許可を必要とします - 追加 - フォルダがありません - このフォルダはサポートされていません - 再生状態を復元できません + フォルダがありません + このフォルダはサポートされていません トラック %d 再生またはポーズ 再生待ちの曲を除去 - フォルダを除去 + フォルダを除去 Auxio アイコン アルバムカバー %s のアルバムカバー @@ -138,11 +132,8 @@ 読み込みが完了したジャンル数: %d -%.1f デシベル 読み込みが完了したアルバム数: %d - 再生状態の解除完了 - 再生状態の復元完了 リセット Auxioについて - 再生状態の保存完了 ソースコード Wiki バージョン @@ -155,13 +146,9 @@ ライブラリからの再生時 アイテム詳細からの再生時 音楽以外を除外 - ここに追加したフォルダからのみ音楽が読み込まれます。 - 前回保存された再生状態がある場合、再生状態を復元 このタスクを実行できるアプリが見つかりません コンテンツ 音楽の再スキャン - 再生状態を解除 - 前回保存された再生状態を解除 Matroska オーディオ 高度なオーディオ コーデック (AAC) 品質を損なうことのない無料のオーディオ コーデック (FLAC) @@ -187,16 +174,14 @@ オーディオ 再生 ポーズと繰り返し - ここに追加したフォルダはからは音楽が読み込まれません ライブラリ タグ無しで調整 - フォルダ + フォルダ セミコロン (;) スラッシュ (/) 繰り返しモードを変更 シャフルのオン・オフ 次の曲にスキップ - 再生状態を保存できません すべての曲をシャフル %d ヘルツ %d kbps @@ -227,7 +212,6 @@ ジャンルから再生 新しい曲の再生時にシャフルを保持 ダイナミック - 再生状態を解除できません 再生中の音楽がありません MPEG-4 オーディオ 深緑 @@ -236,19 +220,17 @@ +%.1f デシベル 追加の UI 要素で角丸を有効にします (アルバム カバーを丸める必要があります) 外観 - モード 複数のタグ値を表す文字を構成する カスタム再生バー アクション ソート時に記事を無視する 名前で並べ替えるときに「the」などの単語を無視する (英語の音楽に最適) - 初期 (高速読み込み) + 初期 (高速読み込み) 再生中の場合はアルバムを優先 複数値セパレータ 変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です) サウンドと再生の動作を構成する 戻る前に巻き戻す 警告: プリアンプを高い正の値に変更すると、一部のオーディオ トラックでピーキングが発生する場合があります。 - 再生状況 キューを開く 音楽再生の表示と制御 ラウドネスイコライゼーション @@ -267,7 +249,6 @@ プレイリストに追加されました 曲がありません プレイリスト %d - 新しいプレイリストを作成する 消去 名前の変更 プレイリストの名前を変更する @@ -283,4 +264,27 @@ フォーススクエアのアルバムカバー すべてのアルバム カバーを 1:1 のアスペクト比にトリミングします + Windows互換パスを使用する + 寄付 + サポーター + デモ + デモ + アルバムがありません + プレイリストをインポート + パス + 再生開始 + エラー情報 + コピーしました + パススタイル + インポート + エクスポート + プレイリストをエクスポート + フィードバック + メールを送信 + プレイリストをインポートしました + プレイリストをエクスポートしました + プロジェクトに寄付して、ここにあなたの名前を追加します! + オフ + このファイルからプレイリストをインポートできません + このファイルにプレイリストをエクスポートできません \ No newline at end of file diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml new file mode 100644 index 000000000..954c3d4a9 --- /dev/null +++ b/app/src/main/res/values-ka/strings.xml @@ -0,0 +1,22 @@ + + + გამეორება + მეტი + EP + მარტივი და სასიამოვნო მუსიკის დამკვრელი Android-სთვის. + სიმღერა + EP-ის რემიქსი + მუსიკის ჩატვირთვა + სიმღერები + მუსიკის ბიბლიოთეკის მონიტორინგი + ავტორიზაცია + ალბომები + EP-ები + ყველა სიმღერა + ალბომი + შეარჩიე საქაღალდეები + მუსიკის ჩატვირთვა + პირდაპირი EP + სინგლები + სინგლი + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bae32c1d8..bd47b3560 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -36,7 +36,6 @@ 대기열에 추가했습니다. 아티스트로 이동 앨범으로 이동 - 상태 저장됨 확인 @@ -86,8 +85,6 @@ 반복 재생 시 일시 중지 곡이 반복 재생될 때 일시 중지 내용 - 재생 상태 저장 - 현재 재생 상태를 지금 저장합니다. 음악 새로고침 캐시된 태그를 사용하여 음악 라이브러리를 다시 불러옵니다. @@ -95,8 +92,8 @@ 음악 불러오기 실패 앱에서 음악 라이브러리를 읽을 수 있는 권한이 필요합니다. 이 작업을 처리할 수 있는 앱을 찾을 수 없습니다. - 폴더 없음 - 지원하지 않는 폴더입니다. + 폴더 없음 + 지원하지 않는 폴더입니다 라이브러리에서 검색… @@ -111,7 +108,7 @@ 이 곡 이동 이 탭 이동 검색 기록 삭제 - 폴더 제거 + 폴더 제거 Auxio 아이콘 앨범 커버 %s의 앨범 커버 @@ -158,9 +155,6 @@ MPEG-4 오디오 Free Lossless Audio Codec (FLAC) - 이전에 저장된 재생 상태 초기화 - 제외 - 추가한 폴더에서만 음악을 불러옵니다. 곡 속성 속성 보기 샘플 속도 @@ -179,12 +173,8 @@ 무작위 재생 표시된 항목에서 재생 음악 라이브러리 불러오는 중… - 재생 상태 지우기 - 재생 상태 복원 - 음악 폴더 - 음악을 불러오는 위치 관리 - 추가한 폴더에서 음악을 불러오지 않습니다. - 포함 + 음악 폴더 + 음악을 불러오는 위치 관리 다중 값 구분 기호 태그 값이 여러 개일 때 태그를 구분할 기호를 설정합니다. 콤마 (,) @@ -211,16 +201,12 @@ Advanced Audio Coding (AAC) 앨범 커버 - 빠름 - 고품질 - 이전에 저장된 재생 상태 복원 - 재생 상태를 복원할 수 없습니다. + 빠름 + 고품질 음악 라이브러리가 변경될 때마다 새로고침 (고정 알림 필요) - 상태 지워짐 음악 불러오는 중 음악 불러오는 중 음악 라이브러리 모니터링 중 - 상태 복원됨 EP 앨범 EP 앨범 싱글 @@ -232,7 +218,6 @@ 믹스테이프 리믹스 자동 새로고침 - 모드 음악 라이브러리를 불러오는 중… (%1$d/%2$d) 장르 경고: 이 설정을 사용하면 몇몇 태그가 다중 값을 가진 것으로 잘못 나타날 수 있습니다. 태그에서 구분 기호 앞에 백슬래시(\\)를 붙이면 구분 기호로 인식하지 않습니다. @@ -242,8 +227,6 @@ 팟캐스트 등 음악이 아닌 오디오 파일 무시 공동 작업자 숨기기 앨범에 등장하는 아티스트만 표시 (자세히 태그된 라이브러리에 최적화) - 재생 상태를 지울 수 없습니다. - 재생 상태를 저장할 수 없습니다. 음악 재탐색 %d 아티스트 @@ -254,16 +237,15 @@ 위키 장르에서 재생 %1$s, %2$s - ReplayGain + ReplayGain 볼륨 조정 사운드 및 재생 동작 구성 재생 - 폴더 + 폴더 앱 테마 및 색상 변경 음악 라이브러리 이미지 음악 및 이미지 불러오기 방식 설정 - 지속 동작 UI 제어 및 동작 사용자 정의 내림차순 @@ -272,7 +254,6 @@ %s의 재생 목록 이미지 적응형 정렬 정렬할 때 숫자나 \"the\"와 같은 단어를 무시합니다. 태그가 영어로 되어 있을 때 가장 잘 작동합니다. - 새 재생 목록 만들기 새 재생 목록 재생 목록에 추가 재생 목록을 만들었습니다. @@ -330,4 +311,14 @@ ReplayGain 트랙 조절값 ReplayGain 앨범 조절값 일시 중지 기억 + 끄기 + 재생 시작 + 이전에 저장된 상태에 따라 Auxio를 시작합니다. 저장된 상태가 없으면 모든 곡을 무작위 재생합니다. 재생은 즉시 시작됩니다. +\n +\n경고: 이 서비스는 주의하여 사용하세요. 서비스를 닫은 뒤 다시 사용하려고 할 경우 앱이 충돌할 수 있습니다. + 더 보기 + 폴더 선택 + 전자우편을 전송합니다 + 깃허브에 문제를 제기합니다 + 응답 \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 15c4f41fd..c7e5d0f38 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -5,7 +5,7 @@ Paieška Filtruoti Visi - Rūšiavimas + Rūšiuoti Pavadinimas Data Trukmė @@ -13,14 +13,14 @@ Diskas Pridėta data Didėjantis - Groti kitą + Leisti kitą Pridėti į eilę Eilė Eiti į atlikėją Eiti į albumą Peržiūrėti ypatybes Dydis - Bitų srautas + Bitų sparta Skaitmeninimo dažnis Automatinis Šviesi @@ -30,12 +30,12 @@ Atlikėjai Albumai Takelis - Dabar groja - Groti + Dabar leidžiama + Leisti Licencijos Maišyti Pridėta į eilę - Dainų ypatybės + Dainos ypatybės Išsaugoti Apie Pridėti @@ -44,15 +44,15 @@ Versija Nustatymai Tema - Naudoti grynai juodą tamsią temą. - Paprastas, racionalus Android muzikos grotuvas. - Muzikos pakrovimas - Peržiūrėk ir valdyk muzikos grojimą + Naudok grynai juodą tamsią temą. + Paprastas, racionalus „Android“ muzikos leistuvė. + Muzikos įkėlimas + Peržiūrėk ir valdyk muzikos įrašo perklausą Žanrai Pakartoti Suteikti - Kraunama muzika - Kraunama tavo muzikos biblioteka… + Įkeliama muzika + Įkeliamas tavo muzikos biblioteka… Bibliotekos statistika Rožinis Albumas @@ -70,19 +70,17 @@ Nežinomas atlikėjas Albumo viršelis Giliai violetinė - Stebėjimas muzikos biblioteka + Stebima muzikos biblioteka Stebimas tavo muzikos biblioteka dėl pakeitimų… Maišyti Maišyti viską - Atkurta būsena - Išsaugota būsena Atšaukti Šaltinio kodas Rodinys - ReplayGain strategija + „ReplayGain“ strategija Singlai Gerai - Įjungti suapvalintų kampų papildomiems UI elementams (reikia, kad albumo viršeliai būtų suapvalinti). + Įjunk suapvalintų kampų papildomiems naudotojo sąsajos elementams (reikia, kad albumo viršeliai būtų suapvalinti). Garso takelis Garso takeliai Garsas @@ -120,28 +118,28 @@ Gyvai albumas Remikso albumas Gyvai - Visada pradėti groti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose). + Visada pradėk leisti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose). Ogg garsas Aleksandras Keiphartas (angl. Alexander Capehart) Pageidauti takeliui - Nėra aplankų - Šis aplankas nepalaikomas. - Groti arba pristabdyti + Nėra aplankų + Šis aplankas nepalaikomas. + Leisti arba pristabdyti Praleisti į kitą dainą Praleisti į paskutinę dainą - Mikstapas - Mikstapai + Mikso juosta + Misko juostos Bibliotekos skirtukai - Keisti bibliotekos skirtukų matomumą ir tvarką. + Keisk bibliotekos skirtukų matomumą ir tvarką. Pageidauti albumui Pageidauti albumui, jei vienas groja Programėlę nerasta, kuri galėtų atlikti šią užduotį. - Auxio piktograma + „Auxio“ piktograma Perkelti šią dainą Perkelti šį skirtuką - Muzikos pakrovimas nepavyko. - Auxio reikia leidimo skaityti tavo muzikos biblioteką. - Diskas %d + Nepavyko įkelti muzikos. + „Auxio“ reikia leidimo skaityti tavo muzikos biblioteką. + %d diskas +%.1f dB -%.1f dB Bendra trukmė: %s @@ -150,61 +148,53 @@ Rinkiniai Rinkinys Prisiminti maišymą - Palikti maišymą įjungtą, kai groja nauja daina. - Persukti prieš praleistant atgal - Persukti atgal prieš praleistant į ankstesnę dainą. + Palik maišymą įjungtą, kai groja nauja daina. + Persukti prieš praleidžiant atgal + Persuk atgal prieš praleidžiant į ankstesnę dainą. Pauzė ant kartojimo - Kai grojant iš bibliotekos - Kai grojant iš elemento detalių - Pašalinti aplanką + Kai leidžiant iš bibliotekos + Kai leidžiant iš elemento informacijos + Pašalinti aplanką Žanras Ieškok savo bibliotekoje… Ekvalaizeris - Režimas - Automatinis perkrauvimas - Muzikos nerasta. - Sustabdyti grojimą + Automatinis perkėlimas + Muzika nerasta. + Sustabdyti įrašo perklausą Nėra takelio Praleisti į kitą - Automatinis ausinių grojimas + Automatinis ausinių leidimas Kartojimo režimas Atidaryti eilę Išvalyti paieškos užklausą - Muzika nebus kraunama iš pridėtų aplankų, kurių tu pridėsi. - Įtraukti Pašalinti šią dainą - Groti iš visų dainų - Groti iš parodyto elemento - Groti iš albumo - Groti iš atlikėjo - Išvalyta būsena - Neįtraukti - Muzika bus kraunama iš aplankų, kurių tu pridėsi. + Leisti iš visų dainų + Leisti iš parodyto elemento + Leisti iš albumo + Leisti iš atlikėjo %d Hz - Perkrauti muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo). - Pakrautos dainos: %d - Pakrautos žanros: %d - Pakrauti albumai: %d - Pakrauti atlikėjai: %d - Kraunama tavo muzikos biblioteka… (%1$d/%2$d) + Perkelk muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo). + Įkeltos dainos: %d + Įkeltos žanros: %d + Įkelti albumai: %d + Įkelti atlikėjai: %d + Įkėliamas tavo muzikos biblioteka… (%1$d/%2$d) Maišyti visas dainas Suasmeninti Įspėjimas: keičiant išankstinį stiprintuvą į didelę teigiamą reikšmę, kai kuriuose garso takeliuose gali atsirasti tarpų. Albumo viršelis %s Atlikėjo vaizdas %s - Nėra grojančio muzikos - Sustabdyti, kai daina kartojasi. + Nėra leidžiamos muzikos + Sustabdyk, kai daina kartojasi. Turinys - Muzikos aplankai + Muzikos aplankai Atnaujinti muziką - Perkrauti muzikos biblioteką, naudojant talpyklos žymes, kai įmanoma. - Pasirinktinis grojimo juostos veiksmas - Nepavyksta atkurti būsenos. - ReplayGain išankstinis stiprintuvas - Išsaugoti grojimo būseną - Tvarkyti, kur muzika turėtų būti kraunama iš. + Perkrauk muzikos biblioteką, naudojant podėlio žymes, kai įmanoma. + Pasirinktinis įrašo perklausos juostos veiksmas + „ReplayGain“ išankstinis stiprintuvas + Tvarkyk, kur muzika turėtų būti įkeliama iš. Žanro vaizdas %s - Įjungti maišymą arba išjungti + Įjungti arba išjungti maišymą %d takelis Keisti kartojimo režimą Indigos @@ -213,32 +203,25 @@ DJ miksas Gyvai rinkinys Remikso rinkinys - Išvalyti anksčiau išsaugotą grojimo būseną (jei yra). - Daugiareikšmiai separatoriai + Daugiareikšmiai skirtukai Pasvirasis brūkšnys (/) Pliusas (+) Ampersandas (&) Albumų viršeliai Išjungta - Greitis - Išsaugoti dabartinę grojimo būseną dabar. - Išvalyti grojimo būseną - Konfigūruoti simbolius, kurie nurodo kelias žymių reikšmes. + Greita + Konfigūruok simbolius, kurie nurodo kelias žymių reikšmes. Kablelis (,) - Koregavimas be žymių + Koreguoti be žymių Įspėjimas: naudojant šį nustatymą, kai kurios žymes gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus su agalinių brūkšniu (\\). Kabliataškis (;) - Aukštos kokybės - Atkurti grojimo būseną + Aukštos kokybės Neįtraukti nemuzikinių - Ignoruoti garso failus, kurie nėra muzika, tokius kaip tinklalaides. - Išankstinis stiprintuvas taikomas esamam koregavimui grojimo metu. - Koregavimas su žymėmis - Atkurti anksčiau išsaugotą grojimo būseną (jei yra). + Ignoruok garso failus, kurie nėra muzika, tokius kaip tinklalaides. + Išankstinis stiprintuvas taikomas esamam koregavimui įrašo perklausos metu. + Koreguoti su žymėmis Slėpti bendradarbius - Rodyti tik tuos atlikėjus, kurie yra tiesiogiai įtraukti į albumą (geriausiai veikia gerai pažymėtose bibliotekose). - Nepavyksta išvalyti būsenos. - Nepavyksta išsaugoti būsenos. + Rodyk tik tuos atlikėjus, kurie yra tiesiogiai įvardyti į albumą (geriausiai veikia gerai pažymėtose bibliotekose). %d atlikėjas %d atlikėjai @@ -246,31 +229,29 @@ %d atlikėjų Perskenuoti muziką - Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau užbaigta). + Išvalyk žymių podėlį ir pilnai perkrauk muzikos biblioteką (lėčiau, bet labiau užbaigta). %d pasirinkta - Groti iš žanro + Leisti iš žanro Viki %1$s, %2$s Atkurti Biblioteka Elgesys Pakeisk programėlės temą ir spalvas. - Valdyk, kaip muzika ir vaizdai kraunami. - Konfigūruok garso ir grojimo elgesį. + Valdyk, kaip muzika ir vaizdai įkeliami. + Konfigūruok garso ir įrašo perklausos elgesį. Pritaikyk naudotojo sąsajos valdiklius ir elgseną. Muzika Vaizdai - Grojimas - ReplayGain - Aplankai - Pastovumas + Įrašo perklausa + Garso normalizavimas + Aplankai Mažėjantis - Teisingai surūšiuoti pavadinimus, kurie prasideda skaičiais arba žodžiais, tokiais kaip „the“ (geriausiai veikia su anglų kalbos muzika). + Teisingai surūšiuok pavadinimus, kurie prasideda skaičiais arba žodžiais, tokiais kaip „the“ (geriausiai veikia su anglų kalbos muzika). Išmanusis rūšiavimas Grojaraštis Grojaraščiai Grojaraščio vaizdas %s - Kurti naują grojaraštį Naujas grojaraštis Pridėti į grojaraštį Pridėta į grojaraštį @@ -287,13 +268,13 @@ Sukurtas grojaraštis Ištrintas grojaraštis Nėra disko - Redaguojama %s + %s redaguojama Pasirodo Daina Peržiūrėti - Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento. + Apkarpyk visus albumų viršelius iki 1:1 kraštinių koeficiento. Priversti kvadratinių albumų viršelius - Groti dainą pačią + Leisti dainą pačią Rūšiuoti pagal Kryptis Pasirinkimo vaizdas @@ -303,14 +284,14 @@ Daugiau Pranešti Nėra albumų - Demo - Demos + Demo versija + Demo versijos Importuotas grojaraštis Importuotas grojaraštis Eksportuotas grojaraštis - Nepavyksta eksportuoti grojaraščio į šį failą - ReplayGain takelio koregavimas - ReplayGain albumo koregavimas + Nepavyksta eksportuoti grojaraščio į šį failą. + „ReplayGain“ takelio koregavimas + „ReplayGain“ albumo koregavimas Autorius Aukoti Palaikytojai @@ -325,7 +306,28 @@ Kelio stilius Absoliutinis Santykinis - Naudoti Windows suderinamus kelius + Naudoti „Windows“ suderinamus kelius Prisiminti pauzę - Išlieka grojimas ir (arba) pristabdomas, kai praleidžiama arba redaguojama eilė. + Išlaikyk leidimą / pristabdymą, kai praleidžiama arba redaguojama eilė. + Išjungta + Paleidžia „Auxio“ naudojant anksčiau išsaugotą būseną. Jei nėra išsaugotos būsenos, visos dainos bus išmaišytos. Įrašo perklausa bus pradėta iš karto. +\n +\nĮSPĖJIMAS: būk atsargus (-i) valdant šią paslaugą, jei ją uždarysi ir vėl bandysi naudoti, tikriausiai sutriks programėlė. + Paleisti įrašo perklausą + Daugiau + Atsiliepimai + Sukurti problemą svetainėje „GitHub“ + MPEG-4, kuriame yra %s + Nežinomas albumas + Čia bus rodomos jūsų dainos. + Čia bus rodomi jūsų albumai. + Čia bus rodomi jūsų atlikėjai. + Čia bus rodomi jūsų grojaraščiai. + Čia bus rodomi jūsų žanrai. + Pasirinkti aplankus + Siųsti el. laišką + Naujas aplankas + „Apple“ be nuostolių garso kodekas (ALAC) + Nežinomas + Išsaugoti vietos \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-lv/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b93ec52f9..f363362d8 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -36,12 +36,10 @@ ക്രമീകരണങ്ങൾ സംഗീതം ചിത്രങ്ങൾ - വേഗം - ഉയർന്ന നിലവാരമുള്ളത് + വേഗം + ഉയർന്ന നിലവാരമുള്ളത് അടുത്ത പാട്ടിലേക്ക് പോകുക ഗ്രന്ഥശാല - ഒഴിവാക്കുക - സ്ഥിരോത്സാഹം പച്ച അവസാന പാട്ടിലേക്ക് പോകുക കളിക്കുക അല്ലെങ്കിൽ താൽക്കാലികമായി നിർത്തുക @@ -87,11 +85,8 @@ കലാകാരനിലേക്ക് പോകുക സവിശേഷതകൾ കാണുക - സ്ഥിതി സംരക്ഷിച്ചു അവരോഹണം - സ്ഥിതി പുനഃസ്ഥാപിച്ചു വിക്കി - സ്ഥിതി മായ്ച്ചു തത്സമയം തത്സമയ സമാഹാരം ഗീതം diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 85c0607ee..cb76f29f3 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,18 +1,18 @@ Legg til i kø - Personaliser + Personalisér Artister innlastet: %d Legg til i spilleliste Kildekode Lisenser Lys Automatisk - ReplayGain + Volumnormalisering Gjenoppfrisk musikk Justering uten etiketter Flytt denne fanen - Tøm søk + Tøm søket Auxio-ikon Åpne køen Ingen dato @@ -29,14 +29,14 @@ Artist Artister Sjanger - Sjangere + Sjangrer Ny spilleliste Søk - Kompilasjoner - Kompilasjon - Live-kompilasjon + Sammenstillinger + Sammenstilling + Live-sammenstilling Remiks-kompilasjon - Bidrar på + Fremtrer på Alle Navn Varighet @@ -44,7 +44,6 @@ Spill neste Bibliotek - Kunne ikke lagre tilstand %d artist %d artister @@ -53,87 +52,77 @@ %d spor %d spor - Filter + Filtrér Spilleliste Spillelister Slett - Disk + Platenummer Gå til artist Gå til album Vis Del Størrelse Bitrate - Samplerate - Omstokking - Omstokk alt + Samplingsfrekvens + Omstokk + Omstokk alle Avbryt Om Vis og kontroller musikkavspilling Tillagt i kø Gjentagelsesmodus Tilpass grensesnittskontroller og adferd - Utseende og adferd + Utseende Avrundede hjørner i ytterligere grensesnittselementer (krever at albumsomslag er avrundet) Behold omstokking ved avspilling av et nytt spor Husk omstokking Skråstrek (/) Plusstegn (+) - Skjul bidragsytere + Skjul medvirkende Bilder - Albumsomslag - Rask - Høy kvalitet - Alltid start avspilling når hodetelefoner kobles til (trenger ikke å virke på alle enheter) + Albumomslag + Raskt + Høy kvalitet + Alltid start avspilling når hodetelefoner kobles til (virker kanskje ikke på alle enheter) Sett opp lyd- og avspillingsadferd Lyd ReplayGain-strategi - Håndter hvor musikk lastes inn fra + Håndter hvor musikk lastes inn fra Forforsterkning brukes for eksisterende justering under avspilling - Modus - Lagre nåværende avspillingstilstand nå - Lagre avspillingstilstand Tøm etiketthurtiglager og last inn hele musikkbiblioteket igjen (tregere, men mer fullstendig) - Vedvarende Spor %d - Albumsomslag + Albumomslag Skru omstokking på eller av Fjern dette sporet Flytt dette sporet - Ingen disk - Ingen spor + Ingen plate + Intet spor Avansert audio-koding (AAC) Dynamisk %d valgt Laster inn musikkbiblioteket ditt … (%1$d/%2$d) %d Hz - Slett %s for godt\? - Gjenopprett tidligere lagret avspillingstilstand (hvis noen) + Slett %s for godt? Dette kan ikke angres. MPEG-4-lyd Spilleliste %d Cyanblå Spor innlastet: %d Dato Endre drakten og programfargene - Mapper - Tøm avspillingstilstand - Fjern tidligere lagret avspillingstilstand (hvis noen) + Mapper Gul Intelligent sortering Gi nytt navn Gi spillelisten nytt navn Slett spilleliste\? OK - Tilstand lagret Versjon Wiki Tilbakestill Legg til - Tilstand fjernet - Tilstand gjenopprettet Fargedrakt Svart drakt - Ifør helsvart drakt + Bruk en helsvart mørk drakt Drakt Mørk Musikk @@ -141,43 +130,39 @@ Komma (,) Semikolon (;) Automatisk gjeninnlasting - Ignorer lydfiler som ikke er musikk, som f-eks. nettradioopptak + Ignorer lydfiler som ikke er musikk, som f.eks. podkaster Multiverdi-inndelere Sett opp tegn for inndeling av flere etikett-verdier - Kun vis artister som er kreditert direkte på album (fungerer best med godt etikettmerkede bibliotek) + Vis kun artister som er kreditert direkte på album (fungerer best med velmerkede bibliotek) Reskann musikk - Musikk vil kun innlastes fra mappene du legger til. Last inn musikkbiblioteket igjen, ved bruk av hurtiglagrede etiketter når mulig - Ingen mapper - Kunne ikke gjenopprette tilstand - Kunne ikke fjerne tilstand + Ingen mapper Album innlastet: %d - Biblioteksstatistikk + Bibliotekstatistikk Av Avrundede hjørner ReplayGain-forforsterkning Justering med etiketter Adferd Innhold - Musikkmapper - Gjeninnlast musikkbibliotek når det endrer seg (krever vedvarende merknad) - Kunne ikke laste inn musikk - Denne mappen støttes ikke + Musikkmapper + Gjeninnlast musikkbibliotek når det endrer seg (krever vedvarende varsling) + Klarte ikke laste inn musikk + Denne mappen støttes ikke Hopp til neste spor Hopp til siste spor Omstokk alle spor - Fjern mappe + Fjern mappe Ukjent sjanger Sjangerbilde for %s Ukjent artist Sjangere innlastet: %d Stopp avspilling - Opprett en ny spilleliste Grønn Mørkegrønn - Turkis + Blågrønn Rediger - Albumsomslag for %s + Albumomslag for %s Lilla Blå Fritt tapsfritt lydkodek (FLAC) @@ -187,21 +172,21 @@ DJ-miks Live Spilles nå - Omstokking + Omstokk Stigende Format Vis egenskaper - Spor-egenskaper + Sporegenskaper Pause ved gjentagelse Rød %d album %d album - Synkende + Fallende Spor Dato tillagt - Sorter + Sortér Utviklet av Alexander Capehart Søk i biblioteket ditt … Innstillinger @@ -211,21 +196,18 @@ Spilleliste slettet Lagt til i spilleliste Hopp til neste - Egendefinert merknadshandling + Egendefinert varslingshandling Foretrekk spor Pause når et spor gjentas Spol tilbake før spor hoppes over Spol tilbake før hopp til forrige spor Advarsel: Endring av forforsterkning til høy positiv verdi kan resultere i forvrengning ved høyt lydtrykk på noen spor. Avspilling - Disk %d + Plate %d Skjerm - Biblioteksfaner - Endre synlighet og rekkefølgen på biblioteksfaner + Bibliotekfaner + Endre synlighet og rekkefølgen på bibliotekfaner Egendefinert avspillingsfelt-handling - Utelat - Inkluder - Musikk vil ikke innlastes fra mappene du legger til. Ingen spor Ingen musikk spilles Oransje @@ -233,12 +215,11 @@ Spill fra album Spill fra sjanger Foretrekk album - Gjenopprett avspillingstilstand Ampersand (&) Spill sporet for seg selv - Påtving kvadratiske albumsomslag + Påtving kvadratiske albumomslag Korrekt sortering av navn som begynner med tall eller ord som «the» (fungerer best med engelskspråklig musikk) - Beskjær alle albumsomslag til 1:1-sideforhold + Beskjær alle albumomslag til 1:1-sideforhold Spill av eller pause Artistbilde for %s Auxio trenger tilgang til å lese musikkbiblioteket ditt @@ -254,11 +235,11 @@ Album Live-EP Spill fra alle spor - Laster inn musikk … + Musikk lastes inn Live-singel - Laster inn musikk … - Holder øye med musikkbiblioteket … - Innvilg + Laster inn musikk + Holder øye med musikkbiblioteket + Bevilge Singler Spor Album @@ -279,13 +260,55 @@ Fant ikke noe musikk Matroska-lyd Spill fra vist element - Ogg-lyd + OGG-lyd Mørkeblå - Foretrekk album hvis det avspilles - Hodesett-autoavspilling + Foretrekk album hvis et album spilles av + Automatisk avspilling med headsett Spillelistebilde for %s Kontroller hvordan musikk og bilder innlastes Installer et program som kan utføre denne handlingen først - Advarsel: Kan forårsake feilaktig tolkning av etiketter som om de har flere verdier. Kan løses ved å innlede uønskede inndelertegn med omvendt skråstrek (\\). + Advarsel: Hvis denne innstillingen er påslått, kan enkelte etiketter feiltolkes som om de har flere verdier. Dette kan løses ved å legge en omvendt skråstrek (\\) foran skilletegn som ikke skal skille verdier. %d kbps + Av + Bane + Utvalg + Eksportér spilleliste + ReplayGain-sporjustering + Absolutt + Bruk Windows-kompatible baner + Banestil + Relativt + Forbli spillende/pauset ved sporbytting eller køredigering + Spilleliste importert + Importert spilleliste + Spilleliste eksportert + Kan ikke importere en spilleliste fra denne filen + Kan ikke eksportere spillelisten til denne filen + Demo + Demoer + Vis mer + Feilopplysninger + Kopiert + ReplayGain-albumjustering + Forfatter + Donér + Støttespillere + Donér til prosjektet slik at navnet ditt kan legges til her! + Rapportér + Husk pause + Utvalgsbild + Ingen album + Tom spilleliste + Importér spilleliste + Sortér etter + Retning + Importér + Eksportér + Mer + Begynner Auxio med den forrige lagrede tilstanden. Dersom ingen lagrede tilstand er tilgjengelig, skal sangene omstokkes. Avspilling begynner med én gang.\n\nADVARSEL: Vær varsom med å kontrollere denne tjenesten, hvis du lukker den og så prøver å bruke den igjen, skal det nok få appen til å krasje. + Begynn avspilling + Opprett en sak på GitHub + Gi tilbakemelding + Send en e-post + Velg mapper \ No newline at end of file diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml new file mode 100644 index 000000000..65aac81c4 --- /dev/null +++ b/app/src/main/res/values-ne/strings.xml @@ -0,0 +1,67 @@ + + + गीतहरु लोड हुँदै छन् + एन्डरोइडको लागि एक सहज, विवेकशील गीत बजाउने एप। + साउन्डट्रयाक + सिङ्गल + लाइभ ईपी + डेमोहरु + रीमिक्स सङ्कलन + डिस्क + प्लेलिस्ट एक्सपोर्ट गर्नुहोस् + सबै + मिति + प्लेलिस्टको नाम परिवर्तन गर्नुहोस् + प्लेलिस्टहरु + खाली प्लेलिस्ट + पुनः प्रयास गर्नुहोस् + प्लेलिस्ट इम्पोर्ट गर्नुहोस् + एल्बम + गीतहरु लोड गर्दै + सङ्गीत लाइब्रेरी निगरानी गर्दै + फोल्डर छान्नुहोस् + अझै + दिनुहोस् + गीतहरु + गीत + सबै गीत + लाइभ एल्बम + रीमिक्स एल्बम + ईपीहरु + ईपी + रीमिक्स ईपी + सिङ्गलहरु + लाइभ सिङ्गल + सङ्कलनहरु + लाइभ सङ्कलन + साउन्डट्रयाकहरु + मिक्सटेपहरु + मिक्सटेप + डेमो + डिजे मिक्सहरु + लाइभ + रीमिक्सहरु + यीमा देखिन्छन् + कलाकार + कलाकारहरु + विधा + विधाहरु + प्लेलिस्ट + नयाँ प्लेलिस्ट + इम्पोर्ट गरिएको प्लेलिस्ट + इम्पोर्ट गर्नुहोस् + एक्सपोर्ट गर्नुहोस् + नाम परिवर्तन गर्नुहोस् + हटाउनुहोस् + साँचै प्लेलिस्ट हटाउने? + सम्पादन गर्नुहोस् + खोज्नुहोस् + अवधि + गीतको सङ्ख्या + डिजे मिक्स + नाम + छान्नुहोस् + एल्बमहरु + रीमिक्स सिङ्गल + सङ्कलन + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml deleted file mode 100644 index 10dfde63d..000000000 --- a/app/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,399 +0,0 @@ - - - - #01000000 - - - @color/material_dynamic_secondary20 - @color/material_dynamic_neutral90 - @color/material_dynamic_neutral20 - - #FFB4A8 - #680001 - #940002 - #FFDAD3 - #BC1714 - #E7BCB6 - #442A26 - #5D3F3B - #FFDAD4 - #DFC38C - #3F2E04 - #574419 - #FCDFA6 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #211A19 - #EDE0DE - #534341 - #D8C2BF - #EDE0DE - #362F2E - - #FFB2C0 - #670024 - #900036 - #FFD9DF - #BC0049 - #E5BDC2 - #43292D - #5C3F43 - #FFD9DE - #EBBF90 - #452B08 - #5F411C - #FFDDB8 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #201A1B - #ECE0E0 - #524345 - #D6C1C3 - #ECE0E0 - #352F2F - - #FBAAFF - #570068 - #7B0091 - #FFD5FF - #9A25AE - #D7BFD5 - #3B2B3B - #534153 - #F5DBF2 - #F6B8AE - #4C251F - #663B34 - #FFDAD2 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1E1A1D - #E9E0E5 - #4D444C - #D0C3CC - #E9E0E5 - #332F32 - - #D4BAFF - #3E008E - #5727A7 - #ECDCFF - #6F43BF - #CDC2DB - #342D41 - #4B4358 - #E9DEF7 - #F0B8C5 - #4A2530 - #643B46 - #FFD9E2 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1D1B1F - #E6E1E5 - #49454E - #CBC4CF - #E6E1E5 - #323033 - - #B9C3FF - #08218A - #293CA0 - #DDE0FF - #4355B9 - #C4C5DD - #2D2F42 - #43465A - #E0E1FA - #E5BAD7 - #45263E - #5D3C55 - #FFD7F3 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1B1B1F - #E4E1E6 - #46464F - #C6C5D0 - #E4E1E6 - #303034 - - #9CCAFF - #00325A - #00497F - #D0E4FF - #0061A6 - #BBC8DB - #253140 - #3C4858 - #D6E3F7 - #D6BEE4 - #3B2948 - #523F5F - #F3DAFF - #FFB4A9 - #930006 - #680003 - #FFDAD4 - #1B1B1B - #E2E2E6 - #42474E - #C3C7D0 - #E2E2E6 - #2F3033 - - #62D3FF - #003546 - #004D64 - #BAE9FF - #006684 - #B4CAD6 - #1F333C - #354A53 - #D0E6F2 - #C5C2EA - #2E2D4C - #454365 - #E3DFFF - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #191C1E - #E1E2E4 - #40484C - #C0C8CD - #E1E2E4 - #2F3132 - - #44D8F1 - #00363F - #004E5A - #9CEFFF - #006877 - #B1CBD1 - #1C3439 - #334A4F - #CDE7ED - #BCC5EA - #262F4D - #3D4665 - #DAE1FF - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #191C1D - #E1E3E3 - #3F484A - #BFC8CA - #E1E3E3 - #2D3132 - - #53DBC9 - #003730 - #005047 - #74F7E5 - #006A5F - #B1CCC6 - #1C3531 - #334B47 - #CDE8E2 - #ADCAE6 - #153349 - #2D4960 - #CBE5FF - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #191C1B - #E0E3E1 - #3F4947 - #BFC9C6 - #E0E3E1 - #2E3130 - - #78DC77 - #003907 - #00530F - #93F990 - #006E17 - #B9CCB3 - #253423 - #3B4B38 - #D5E8CE - #A1CFD5 - #00363B - #1E4D52 - #BCEBF0 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1A1C19 - #E2E3DD - #424840 - #C2C8BD - #E2E3DD - #2F312D - - #9ED75C - #1C3700 - #2C5000 - #B9F475 - #3C6A00 - #BFCAAD - #2A331E - #404A34 - #DBE7C7 - #A0D0CC - #003735 - #1E4E4B - #BCECE8 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1A1C17 - #E3E3DB - #44483D - #C4C8B9 - #E3E3DB - #2F312C - - #C1D02C - #2E3400 - #434B00 - #DEED49 - #5A6400 - #C7C9A6 - #30321A - #46492E - #E4E5C1 - #A3D0C1 - #06372C - #234E43 - #BEECDC - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1C1C17 - #E5E2DA - #47473B - #C8C7B7 - #E5E2DA - #31312B - - #FABD00 - #402D00 - #5C4300 - #FFDF99 - #795900 - #D7C4A0 - #3B2F15 - #52452A - #F4E0BB - #B0CFA9 - #1D361C - #334D31 - #CCEBC5 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1E1B16 - #E9E1D8 - #4D4639 - #D0C5B4 - #E9E1D8 - #3C2E16 - - #FFB86D - #4B2800 - #6A3B00 - #FFDCBB - #E2C1A4 - #402C18 - #8C5000 - #59422C - #FEDCBE - #C0CC9A - #2B3410 - #414B25 - #DCE8B4 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #1F1B17 - #EBE0D9 - #51453A - #D5C3B5 - #EBE0D9 - #352F2B - - #E7BEB0 - #442A20 - #5D4035 - #FFDBCD - #77574C - #FFB598 - #5C1A00 - #7B2E0D - #FFDBCD - #D5C78E - #383005 - #50461A - #F1E2A7 - #FFB4A9 - #680003 - #930006 - #FFDAD4 - #201A18 - #EDE0DC - #52433E - #D8C2BB - #EDE0DC - #362F2D - - #EEEEEE - #424242 - #757575 - #616161 - #F5F5F5 - #B4B4B4 - #1F1F1F - #353535 - #D0D0D0 - #C5C5C5 - #2E2E2E - #454545 - #E3E3E3 - #FFB4B4 - #680000 - #930000 - #FFDADA - #1F1F1F - #E1E1E1 - #484848 - #C8C8C8 - #fafafa - #2D3132 - - @color/m3_ref_palette_dynamic_primary80 - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors_android.xml b/app/src/main/res/values-night/colors_android.xml new file mode 100644 index 000000000..36d82280f --- /dev/null +++ b/app/src/main/res/values-night/colors_android.xml @@ -0,0 +1,15 @@ + + + + #01000000 + + + @color/material_dynamic_secondary20 + @color/material_dynamic_neutral90 + @color/material_dynamic_neutral20 + + @color/m3_ref_palette_dynamic_primary80 + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors_ui.xml b/app/src/main/res/values-night/colors_ui.xml new file mode 100644 index 000000000..87bd21bc7 --- /dev/null +++ b/app/src/main/res/values-night/colors_ui.xml @@ -0,0 +1,2274 @@ + + + #FFB4A9 + #561E18 + #73342C + #FFDAD5 + #E7BDB7 + #442926 + #5D3F3B + #FFDAD5 + #DFC38C + #3E2E04 + #574419 + #FCDFA6 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #1A1110 + #F1DEDC + #1A1110 + #F1DEDC + #534341 + #D8C2BE + #A08C89 + #534341 + #000000 + #F1DEDC + #392E2C + #904A42 + #FFDAD5 + #3B0906 + #FFB4A9 + #73342C + #FFDAD5 + #2C1512 + #E7BDB7 + #5D3F3B + #FCDFA6 + #261A00 + #DFC38C + #574419 + #1A1110 + #423735 + #140C0B + #231918 + #271D1C + #322826 + #3D3231 + #FFBAB0 + #330503 + #CC7B70 + #000000 + #EBC1BB + #26100D + #AE8883 + #000000 + #E3C790 + #1F1500 + #A68E5B + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #1A1110 + #F1DEDC + #1A1110 + #FFF9F8 + #534341 + #DCC6C3 + #B39E9B + #927F7C + #000000 + #F1DEDC + #322826 + #74352D + #FFDAD5 + #2C0101 + #FFB4A9 + #5E241D + #FFDAD5 + #200B08 + #E7BDB7 + #4B2F2B + #FCDFA6 + #191000 + #DFC38C + #453409 + #1A1110 + #423735 + #140C0B + #231918 + #271D1C + #322826 + #3D3231 + #FFF9F8 + #000000 + #FFBAB0 + #000000 + #FFF9F8 + #000000 + #EBC1BB + #000000 + #FFFAF7 + #000000 + #E3C790 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #1A1110 + #F1DEDC + #1A1110 + #FFFFFF + #534341 + #FFF9F8 + #DCC6C3 + #DCC6C3 + #000000 + #F1DEDC + #000000 + #4E1812 + #FFE0DB + #000000 + #FFBAB0 + #330503 + #FFE0DB + #000000 + #EBC1BB + #26100D + #FFE4AF + #000000 + #E3C790 + #1F1500 + #1A1110 + #423735 + #140C0B + #231918 + #271D1C + #322826 + #3D3231 + + #FFB2BD + #561D29 + #72333F + #FFD9DD + #E5BDC1 + #43292D + #5C3F43 + #FFD9DD + #EABF8F + #452B07 + #5E411C + #FFDDB9 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #191112 + #F0DEDF + #191112 + #F0DEDF + #524345 + #D6C2C3 + #9F8C8E + #524345 + #000000 + #F0DEDF + #382E2F + #8E4956 + #FFD9DD + #3B0715 + #FFB2BD + #72333F + #FFD9DD + #2C1519 + #E5BDC1 + #5C3F43 + #FFDDB9 + #2B1700 + #EABF8F + #5E411C + #191112 + #413738 + #140C0D + #22191A + #261D1E + #312829 + #3D3233 + #FFB8C2 + #330310 + #C97A87 + #000000 + #E9C1C5 + #261014 + #AC888C + #000000 + #EFC393 + #241200 + #B08A5E + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #191112 + #F0DEDF + #191112 + #FFF9F9 + #524345 + #DBC6C7 + #B29EA0 + #917F80 + #000000 + #F0DEDF + #312829 + #733440 + #FFD9DD + #2C000B + #FFB2BD + #5D222F + #FFD9DD + #200B0E + #E5BDC1 + #4A2F33 + #FFDDB9 + #1D0E00 + #EABF8F + #4C300D + #191112 + #413738 + #140C0D + #22191A + #261D1E + #312829 + #3D3233 + #FFF9F9 + #000000 + #FFB8C2 + #000000 + #FFF9F9 + #000000 + #E9C1C5 + #000000 + #FFFAF8 + #000000 + #EFC393 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #191112 + #F0DEDF + #191112 + #FFFFFF + #524345 + #FFF9F9 + #DBC6C7 + #DBC6C7 + #000000 + #F0DEDF + #000000 + #4E1623 + #FFDFE2 + #000000 + #FFB8C2 + #330310 + #FFDFE2 + #000000 + #E9C1C5 + #261014 + #FFE2C5 + #000000 + #EFC393 + #241200 + #191112 + #413738 + #140C0D + #22191A + #261D1E + #312829 + #3D3233 + + #EBB5ED + #49204E + #613766 + #FFD6FE + #D7BFD5 + #3B2B3C + #534153 + #F4DBF1 + #F6B8AD + #4C251F + #673B34 + #FFDAD4 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #171216 + #EBDFE6 + #171216 + #EBDFE6 + #4D444C + #D0C3CC + #998D96 + #4D444C + #000000 + #EBDFE6 + #352F34 + #7B4E7F + #FFD6FE + #310937 + #EBB5ED + #613766 + #F4DBF1 + #251626 + #D7BFD5 + #534153 + #FFDAD4 + #33110C + #F6B8AD + #673B34 + #171216 + #3E373D + #110D11 + #1F1A1F + #231E23 + #2E282D + #393338 + #F0B9F1 + #2B0432 + #B280B4 + #000000 + #DCC3D9 + #1F1121 + #A08A9E + #000000 + #FABCB2 + #2C0C07 + #BA837A + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #171216 + #EBDFE6 + #171216 + #FFF9FA + #4D444C + #D4C7D1 + #AB9FA9 + #8B8089 + #000000 + #EBDFE6 + #2E282D + #633867 + #FFD6FE + #24002C + #EBB5ED + #4F2654 + #F4DBF1 + #1A0C1B + #D7BFD5 + #413142 + #FFDAD4 + #250704 + #F6B8AD + #532B25 + #171216 + #3E373D + #110D11 + #1F1A1F + #231E23 + #2E282D + #393338 + #FFF9FA + #000000 + #F0B9F1 + #000000 + #FFF9FA + #000000 + #DCC3D9 + #000000 + #FFF9F8 + #000000 + #FABCB2 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #171216 + #EBDFE6 + #171216 + #FFFFFF + #4D444C + #FFF9FA + #D4C7D1 + #D4C7D1 + #000000 + #EBDFE6 + #000000 + #411947 + #FFDCFD + #000000 + #F0B9F1 + #2B0432 + #F9DFF6 + #000000 + #DCC3D9 + #1F1121 + #FFE0DB + #000000 + #FABCB2 + #2C0C07 + #171216 + #3E373D + #110D11 + #1F1A1F + #231E23 + #2E282D + #393338 + + #D3BCFD + #38265C + #4F3D74 + #EBDDFF + #CDC2DB + #342D40 + #4B4358 + #E9DEF8 + #F0B7C5 + #4A2530 + #643B46 + #FFD9E1 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #151218 + #E7E0E8 + #151218 + #E7E0E8 + #49454E + #CBC4CF + #948F99 + #49454E + #000000 + #E7E0E8 + #322F35 + #68548E + #EBDDFF + #230F46 + #D3BCFD + #4F3D74 + #E9DEF8 + #1F182B + #CDC2DB + #4B4358 + #FFD9E1 + #31101B + #F0B7C5 + #643B46 + #151218 + #3B383E + #0F0D13 + #1D1B20 + #211F24 + #2C292F + #36343A + #D7C0FF + #1D0840 + #9B86C4 + #000000 + #D1C6DF + #191325 + #968DA4 + #000000 + #F5BCC9 + #2B0B16 + #B68390 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #151218 + #E7E0E8 + #151218 + #FFF9FF + #49454E + #CFC8D3 + #A7A1AB + #86818B + #000000 + #E7E0E8 + #2C292F + #513E75 + #EBDDFF + #18023B + #D3BCFD + #3E2C62 + #E9DEF8 + #140E20 + #CDC2DB + #3A3346 + #FFD9E1 + #250611 + #F0B7C5 + #512A36 + #151218 + #3B383E + #0F0D13 + #1D1B20 + #211F24 + #2C292F + #36343A + #FFF9FF + #000000 + #D7C0FF + #000000 + #FFF9FF + #000000 + #D1C6DF + #000000 + #FFF9F9 + #000000 + #F5BCC9 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #151218 + #E7E0E8 + #151218 + #FFFFFF + #49454E + #FFF9FF + #CFC8D3 + #CFC8D3 + #000000 + #E7E0E8 + #000000 + #321F55 + #EEE2FF + #000000 + #D7C0FF + #1D0840 + #EEE2FC + #000000 + #D1C6DF + #191325 + #FFDFE5 + #000000 + #F5BCC9 + #2B0B16 + #151218 + #3B383E + #0F0D13 + #1D1B20 + #211F24 + #2C292F + #36343A + + #BAC3FF + #222C61 + #394379 + #DEE0FF + #C3C5DD + #2D2F42 + #434659 + #E0E1F9 + #E6BAD7 + #44263D + #5D3C55 + #FFD7F1 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #121318 + #E4E1E9 + #121318 + #E4E1E9 + #46464F + #C7C5D0 + #90909A + #46464F + #000000 + #E4E1E9 + #303036 + #515B92 + #DEE0FF + #0B154B + #BAC3FF + #394379 + #E0E1F9 + #181A2C + #C3C5DD + #434659 + #FFD7F1 + #2D1228 + #E6BAD7 + #5D3C55 + #121318 + #39393F + #0D0E13 + #1B1B21 + #1F1F25 + #29292F + #34343A + #C0C7FF + #050F46 + #848DC8 + #000000 + #C8C9E1 + #121526 + #8D8FA6 + #000000 + #EABEDC + #270C22 + #AC85A1 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #121318 + #E4E1E9 + #121318 + #FDFAFF + #46464F + #CBC9D4 + #A3A2AC + #83828C + #000000 + #E4E1E9 + #292A2F + #3B447A + #DEE0FF + #000841 + #BAC3FF + #283267 + #E0E1F9 + #0D1021 + #C3C5DD + #323548 + #FFD7F1 + #21071D + #E6BAD7 + #4B2C43 + #121318 + #39393F + #0D0E13 + #1B1B21 + #1F1F25 + #29292F + #34343A + #FDFAFF + #000000 + #C0C7FF + #000000 + #FDFAFF + #000000 + #C8C9E1 + #000000 + #FFF9F9 + #000000 + #EABEDC + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #121318 + #E4E1E9 + #121318 + #FFFFFF + #46464F + #FDFAFF + #CBC9D4 + #CBC9D4 + #000000 + #E4E1E9 + #000000 + #1C255A + #E4E5FF + #000000 + #C0C7FF + #050F46 + #E4E5FE + #000000 + #C8C9E1 + #121526 + #FFDDF3 + #000000 + #EABEDC + #270C22 + #121318 + #39393F + #0D0E13 + #1B1B21 + #1F1F25 + #29292F + #34343A + + #A1C9FD + #003259 + #1B4975 + #D2E4FF + #BBC7DB + #253141 + #3C4858 + #D7E3F8 + #D7BDE4 + #3B2947 + #533F5F + #F3DAFF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #111418 + #E1E2E8 + #111418 + #E1E2E8 + #43474E + #C3C6CF + #8D9199 + #43474E + #000000 + #E1E2E8 + #2E3135 + #37618E + #D2E4FF + #001C37 + #A1C9FD + #1B4975 + #D7E3F8 + #101C2B + #BBC7DB + #3C4858 + #F3DAFF + #251431 + #D7BDE4 + #533F5F + #111418 + #36393E + #0B0E13 + #191C20 + #1D2024 + #272A2F + #32353A + #A8CEFF + #00172E + #6B93C4 + #000000 + #BFCCDF + #0A1725 + #8592A4 + #000000 + #DCC2E8 + #200F2B + #A088AC + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #111418 + #E1E2E8 + #111418 + #FAFAFF + #43474E + #C7CBD3 + #9FA3AB + #7F838B + #000000 + #E1E2E8 + #272A2F + #1D4A76 + #D2E4FF + #001225 + #A1C9FD + #003863 + #D7E3F8 + #051220 + #BBC7DB + #2B3747 + #F3DAFF + #1A0926 + #D7BDE4 + #412F4D + #111418 + #36393E + #0B0E13 + #191C20 + #1D2024 + #272A2F + #32353A + #FAFAFF + #000000 + #A8CEFF + #000000 + #FAFAFF + #000000 + #BFCCDF + #000000 + #FFF9FB + #000000 + #DCC2E8 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #111418 + #E1E2E8 + #111418 + #FFFFFF + #43474E + #FAFAFF + #C7CBD3 + #C7CBD3 + #000000 + #E1E2E8 + #000000 + #002B4F + #D9E8FF + #000000 + #A8CEFF + #00172E + #DBE8FC + #000000 + #BFCCDF + #0A1725 + #F6DFFF + #000000 + #DCC2E8 + #200F2B + #111418 + #36393E + #0B0E13 + #191C20 + #1D2024 + #272A2F + #32353A + + #8BD0F0 + #003546 + #004D64 + #BEE9FF + #B4CAD6 + #1F333D + #354A54 + #D0E6F2 + #C6C2EA + #2F2D4D + #454364 + #E3DFFF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #0F1417 + #DFE3E7 + #0F1417 + #DFE3E7 + #40484C + #C0C8CD + #8A9297 + #40484C + #000000 + #DFE3E7 + #2C3134 + #136682 + #BEE9FF + #001F2A + #8BD0F0 + #004D64 + #D0E6F2 + #081E27 + #B4CAD6 + #354A54 + #E3DFFF + #1A1836 + #C6C2EA + #454364 + #0F1417 + #353A3D + #0A0F11 + #171C1F + #1B2023 + #262B2E + #303538 + #90D4F4 + #001923 + #5499B7 + #000000 + #B8CEDA + #031922 + #7F949F + #000000 + #CBC6EF + #141231 + #908DB2 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #0F1417 + #DFE3E7 + #0F1417 + #F7FBFF + #40484C + #C4CCD1 + #9CA4A9 + #7C8489 + #000000 + #DFE3E7 + #262B2E + #004E66 + #BEE9FF + #00131C + #8BD0F0 + #003B4E + #D0E6F2 + #00131C + #B4CAD6 + #253942 + #E3DFFF + #0F0D2B + #C6C2EA + #353353 + #0F1417 + #353A3D + #0A0F11 + #171C1F + #1B2023 + #262B2E + #303538 + #F7FBFF + #000000 + #90D4F4 + #000000 + #F7FBFF + #000000 + #B8CEDA + #000000 + #FEF9FF + #000000 + #CBC6EF + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #0F1417 + #DFE3E7 + #0F1417 + #FFFFFF + #40484C + #F7FBFF + #C4CCD1 + #C4CCD1 + #000000 + #DFE3E7 + #000000 + #002E3E + #C9ECFF + #000000 + #90D4F4 + #001923 + #D4EAF7 + #000000 + #B8CEDA + #031922 + #E8E4FF + #000000 + #CBC6EF + #141231 + #0F1417 + #353A3D + #0A0F11 + #171C1F + #1B2023 + #262B2E + #303538 + + #83D2E4 + #00363F + #004E5A + #A4EEFF + #B2CBD1 + #1C3439 + #334A50 + #CDE7ED + #BDC5EB + #262F4D + #3D4565 + #DCE1FF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #0E1416 + #DEE3E5 + #0E1416 + #DEE3E5 + #3F484B + #BFC8CB + #899295 + #3F484B + #000000 + #DEE3E5 + #2B3133 + #006877 + #A4EEFF + #001F25 + #83D2E4 + #004E5A + #CDE7ED + #051F24 + #B2CBD1 + #334A50 + #DCE1FF + #111A37 + #BDC5EB + #3D4565 + #0E1416 + #343A3C + #090F11 + #171D1E + #1B2122 + #252B2C + #303637 + #87D7E8 + #00191F + #4A9CAC + #000000 + #B6CFD5 + #01191E + #7C959B + #000000 + #C1C9EF + #0B1531 + #878FB3 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #0E1416 + #DEE3E5 + #0E1416 + #F6FCFD + #3F484B + #C3CCCF + #9BA4A7 + #7B8587 + #000000 + #DEE3E5 + #252B2C + #00505C + #A4EEFF + #001418 + #83D2E4 + #003C46 + #CDE7ED + #001418 + #B2CBD1 + #223A3F + #DCE1FF + #060F2C + #BDC5EB + #2C3553 + #0E1416 + #343A3C + #090F11 + #171D1E + #1B2122 + #252B2C + #303637 + #F3FCFF + #000000 + #87D7E8 + #000000 + #F3FCFF + #000000 + #B6CFD5 + #000000 + #FCFAFF + #000000 + #C1C9EF + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #0E1416 + #DEE3E5 + #0E1416 + #FFFFFF + #3F484B + #F3FCFF + #C3CCCF + #C3CCCF + #000000 + #DEE3E5 + #000000 + #002F37 + #B4F1FF + #000000 + #87D7E8 + #00191F + #D2EBF2 + #000000 + #B6CFD5 + #01191E + #E1E6FF + #000000 + #C1C9EF + #0B1531 + #0E1416 + #343A3C + #090F11 + #171D1E + #1B2122 + #252B2C + #303637 + + #82D5C7 + #003731 + #005048 + #9EF2E3 + #B1CCC6 + #1C3531 + #334B47 + #CCE8E2 + #ADCAE5 + #143349 + #2D4960 + #CCE5FF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #0E1513 + #DDE4E1 + #0E1513 + #DDE4E1 + #3F4946 + #BEC9C5 + #899390 + #3F4946 + #000000 + #DDE4E1 + #2B3230 + #006B5F + #9EF2E3 + #00201C + #82D5C7 + #005048 + #CCE8E2 + #06201C + #B1CCC6 + #334B47 + #CCE5FF + #001E31 + #ADCAE5 + #2D4960 + #0E1513 + #343B39 + #090F0E + #161D1B + #1A211F + #252B2A + #303634 + #87DACC + #001A17 + #4A9E92 + #000000 + #B5D0CA + #011A17 + #7C9690 + #000000 + #B1CEEA + #001829 + #7794AE + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #0E1513 + #DDE4E1 + #0E1513 + #F6FCF9 + #3F4946 + #C3CDCA + #9BA5A2 + #7B8582 + #000000 + #DDE4E1 + #252B2A + #005249 + #9EF2E3 + #001511 + #82D5C7 + #003E37 + #CCE8E2 + #001511 + #B1CCC6 + #223B36 + #CCE5FF + #001321 + #ADCAE5 + #1B394F + #0E1513 + #343B39 + #090F0E + #161D1B + #1A211F + #252B2A + #303634 + #EBFFFA + #000000 + #87DACC + #000000 + #EBFFFA + #000000 + #B5D0CA + #000000 + #F9FBFF + #000000 + #B1CEEA + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #0E1513 + #DDE4E1 + #0E1513 + #FFFFFF + #3F4946 + #F3FDF9 + #C3CDCA + #C3CDCA + #000000 + #DDE4E1 + #000000 + #00302B + #A3F6E8 + #000000 + #87DACC + #001A17 + #D1EDE6 + #000000 + #B5D0CA + #011A17 + #D4E9FF + #000000 + #B1CEEA + #001829 + #0E1513 + #343B39 + #090F0E + #161D1B + #1A211F + #252B2A + #303634 + + #A2D399 + #0C390E + #255023 + #BDF0B3 + #BACCB3 + #253423 + #3B4B38 + #D6E8CE + #A0CFD4 + #00363B + #1E4D52 + #BCEBF0 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #10140F + #E0E4DA + #10140F + #E0E4DA + #424940 + #C2C8BD + #8C9388 + #424940 + #000000 + #E0E4DA + #2D322B + #3C6838 + #BDF0B3 + #002203 + #A2D399 + #255023 + #D6E8CE + #111F0F + #BACCB3 + #3B4B38 + #BCEBF0 + #002022 + #A0CFD4 + #1E4D52 + #10140F + #363A34 + #0B0F0A + #191D17 + #1D211B + #272B25 + #323630 + #A6D89D + #001C02 + #6E9C67 + #000000 + #BED0B7 + #0C1A0A + #85957F + #000000 + #A5D3D8 + #001A1C + #6B989D + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #10140F + #E0E4DA + #10140F + #F9FCF2 + #424940 + #C6CDC1 + #9EA59A + #7F857A + #000000 + #E0E4DA + #272B25 + #265124 + #BDF0B3 + #001601 + #A2D399 + #133F14 + #D6E8CE + #071406 + #BACCB3 + #2B3A28 + #BCEBF0 + #001416 + #A0CFD4 + #073C41 + #10140F + #363A34 + #0B0F0A + #191D17 + #1D211B + #272B25 + #323630 + #F1FFE9 + #000000 + #A6D89D + #000000 + #F1FFE9 + #000000 + #BED0B7 + #000000 + #EFFEFF + #000000 + #A5D3D8 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #10140F + #E0E4DA + #10140F + #FFFFFF + #424940 + #F7FDF0 + #C6CDC1 + #C6CDC1 + #000000 + #E0E4DA + #000000 + #043208 + #C1F4B7 + #000000 + #A6D89D + #001C02 + #DAECD2 + #000000 + #BED0B7 + #0C1A0A + #C0EFF4 + #000000 + #A5D3D8 + #001A1C + #10140F + #363A34 + #0B0F0A + #191D17 + #1D211B + #272B25 + #323630 + + #B0D18B + #1E3702 + #334E17 + #CBEDA5 + #BFCBAD + #2A331F + #404A34 + #DBE7C8 + #A0CFCC + #003735 + #1F4E4C + #BBECE8 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #12140E + #E2E3D8 + #12140E + #E2E3D8 + #44483D + #C4C8BA + #8E9285 + #44483D + #000000 + #E2E3D8 + #2F312A + #4A672D + #CBEDA5 + #0E2000 + #B0D18B + #334E17 + #DBE7C8 + #151E0B + #BFCBAD + #404A34 + #BBECE8 + #00201F + #A0CFCC + #1F4E4C + #12140E + #373A33 + #0C0F09 + #1A1C16 + #1E211A + #282B24 + #33362E + #B4D58F + #0B1A00 + #7B9A5A + #000000 + #C3CFB1 + #101907 + #89957A + #000000 + #A4D4D0 + #001A19 + #6B9996 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #12140E + #E2E3D8 + #12140E + #FAFCF0 + #44483D + #C9CCBE + #A1A497 + #818578 + #000000 + #E2E3D8 + #282B24 + #344F18 + #CBEDA5 + #071400 + #B0D18B + #233D07 + #DBE7C8 + #0B1404 + #BFCBAD + #2F3924 + #BBECE8 + #001414 + #A0CFCC + #083D3B + #12140E + #373A33 + #0C0F09 + #1A1C16 + #1E211A + #282B24 + #33362E + #F4FFE1 + #000000 + #B4D58F + #000000 + #F4FFE1 + #000000 + #C3CFB1 + #000000 + #EAFFFD + #000000 + #A4D4D0 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #12140E + #E2E3D8 + #12140E + #FFFFFF + #44483D + #F9FCED + #C9CCBE + #C9CCBE + #000000 + #E2E3D8 + #000000 + #183000 + #D0F2A9 + #000000 + #B4D58F + #0B1A00 + #DFEBCC + #000000 + #C3CFB1 + #101907 + #C0F0ED + #000000 + #A4D4D0 + #001A19 + #12140E + #373A33 + #0C0F09 + #1A1C16 + #1E211A + #282B24 + #33362E + + #C3CD7B + #2E3400 + #434B05 + #E0E995 + #C7C9A7 + #2F321A + #46492F + #E3E5C1 + #A2D0C1 + #06372D + #224E43 + #BEECDD + #FFB4AB + #690005 + #93000A + #FFDAD6 + #13140D + #E5E3D6 + #13140D + #E5E3D6 + #47483B + #C8C7B7 + #929283 + #47483B + #000000 + #E5E3D6 + #313128 + #5B631E + #E0E995 + #1A1E00 + #C3CD7B + #434B05 + #E3E5C1 + #1B1D07 + #C7C9A7 + #46492F + #BEECDD + #002019 + #A2D0C1 + #224E43 + #13140D + #393A31 + #0E0F08 + #1B1C14 + #202018 + #2A2B22 + #35352D + #C8D17F + #151800 + #8D964B + #000000 + #CBCDAB + #151804 + #909374 + #000000 + #A6D4C6 + #001B14 + #6D9A8C + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #13140D + #E5E3D6 + #13140D + #FDFBEE + #47483B + #CCCCBB + #A4A494 + #848475 + #000000 + #E5E3D6 + #2A2B22 + #454C06 + #E0E995 + #101300 + #C3CD7B + #333A00 + #E3E5C1 + #101301 + #C7C9A7 + #35381F + #BEECDD + #00150F + #A2D0C1 + #0F3D33 + #13140D + #393A31 + #0E0F08 + #1B1C14 + #202018 + #2A2B22 + #35352D + #FAFFC9 + #000000 + #C8D17F + #000000 + #FBFDD9 + #000000 + #CBCDAB + #000000 + #ECFFF7 + #000000 + #A6D4C6 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #13140D + #E5E3D6 + #13140D + #FFFFFF + #47483B + #FDFCEA + #CCCCBB + #CCCCBB + #000000 + #E5E3D6 + #000000 + #282D00 + #E4EE98 + #000000 + #C8D17F + #151800 + #E7E9C6 + #000000 + #CBCDAB + #151804 + #C2F1E1 + #000000 + #A6D4C6 + #001B14 + #13140D + #393A31 + #0E0F08 + #1B1C14 + #202018 + #2A2B22 + #35352D + + #F0BE6D + #432C00 + #604100 + #FFDEAC + #DBC3A1 + #3D2E16 + #55442A + #F8DFBB + #B5CEA4 + #213618 + #374C2C + #D1EABF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #17130B + #ECE1D4 + #17130B + #ECE1D4 + #4E4539 + #D2C4B4 + #9B8F80 + #4E4539 + #000000 + #ECE1D4 + #362F27 + #7D570D + #FFDEAC + #281900 + #F0BE6D + #604100 + #F8DFBB + #261904 + #DBC3A1 + #55442A + #D1EABF + #0D2005 + #B5CEA4 + #374C2C + #17130B + #3F382F + #120D07 + #201B13 + #241F17 + #2F2921 + #3A342B + #F5C371 + #211400 + #B5893E + #000000 + #E0C7A5 + #201402 + #A38E6E + #000000 + #B9D2A8 + #081A02 + #809871 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #17130B + #ECE1D4 + #17130B + #FFFAF7 + #4E4539 + #D6C9B8 + #ADA191 + #8D8173 + #000000 + #ECE1D4 + #2F2921 + #614200 + #FFDEAC + #1A0F00 + #F0BE6D + #4A3100 + #F8DFBB + #1A0F00 + #DBC3A1 + #43341B + #D1EABF + #041500 + #B5CEA4 + #273B1D + #17130B + #3F382F + #120D07 + #201B13 + #241F17 + #2F2921 + #3A342B + #FFFAF7 + #000000 + #F5C371 + #000000 + #FFFAF7 + #000000 + #E0C7A5 + #000000 + #F2FFE5 + #000000 + #B9D2A8 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #17130B + #ECE1D4 + #17130B + #FFFFFF + #4E4539 + #FFFAF7 + #D6C9B8 + #D6C9B8 + #000000 + #ECE1D4 + #000000 + #3A2600 + #FFE3BB + #000000 + #F5C371 + #211400 + #FDE3BF + #000000 + #E0C7A5 + #201402 + #D5EFC3 + #000000 + #B9D2A8 + #081A02 + #17130B + #3F382F + #120D07 + #201B13 + #241F17 + #2F2921 + #3A342B + + #FFB77B + #4C2700 + #6B3A05 + #FFDCC2 + #E3C0A5 + #412C19 + #5A422E + #FFDCC2 + #C4CB97 + #2D330D + #444A22 + #E0E7B1 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #19120C + #EFE0D6 + #19120C + #EFE0D6 + #51443B + #D6C3B6 + #9E8E82 + #51443B + #000000 + #EFE0D6 + #382F28 + #88511D + #FFDCC2 + #2E1500 + #FFB77B + #6B3A05 + #FFDCC2 + #2A1707 + #E3C0A5 + #5A422E + #E0E7B1 + #191E00 + #C4CB97 + #444A22 + #19120C + #413731 + #140D08 + #221A14 + #261E18 + #312822 + #3C332C + #FFBD87 + #271100 + #C3824A + #000000 + #E7C4A9 + #241203 + #AA8B73 + #000000 + #C8CF9B + #141800 + #8E9565 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #19120C + #EFE0D6 + #19120C + #FFFAF8 + #51443B + #DAC7BA + #B1A094 + #908075 + #000000 + #EFE0D6 + #312822 + #6D3C06 + #FFDCC2 + #1F0C00 + #FFB77B + #552C00 + #FFDCC2 + #1E0D01 + #E3C0A5 + #48311F + #E0E7B1 + #0F1300 + #C4CB97 + #333913 + #19120C + #413731 + #140D08 + #221A14 + #261E18 + #312822 + #3C332C + #FFFAF8 + #000000 + #FFBD87 + #000000 + #FFFAF8 + #000000 + #E7C4A9 + #000000 + #F9FFCD + #000000 + #C8CF9B + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #19120C + #EFE0D6 + #19120C + #FFFFFF + #51443B + #FFFAF8 + #DAC7BA + #DAC7BA + #000000 + #EFE0D6 + #000000 + #432100 + #FFE1CC + #000000 + #FFBD87 + #271100 + #FFE1CC + #000000 + #E7C4A9 + #241203 + #E4ECB5 + #000000 + #C8CF9B + #141800 + #19120C + #413731 + #140D08 + #221A14 + #261E18 + #312822 + #3C332C + + #E7BDB0 + #442A21 + #6D4E43 + #FFF7F5 + #D8C2BB + #3B2D29 + #483A35 + #E2CCC5 + #CFC7A1 + #353116 + #5B5537 + #FFF8E3 + #FFB4AB + #690005 + #93000A + #FFDAD6 + #161312 + #E9E1DF + #161312 + #E9E1DF + #504440 + #D4C3BE + #9D8E89 + #504440 + #000000 + #E9E1DF + #33302E + #77574C + #FFDBCF + #2C160D + #E7BDB0 + #5D4036 + #F5DED6 + #251915 + #D8C2BB + #53433E + #ECE3BB + #201C04 + #CFC7A1 + #4C472A + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 + #EBC2B4 + #261009 + #AD887C + #000000 + #DCC6BF + #1F1410 + #A08D86 + #000000 + #D4CBA5 + #1A1602 + #98916E + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #161312 + #E9E1DF + #161312 + #FFF9F8 + #504440 + #D8C7C2 + #AFA09B + #8E807C + #000000 + #E9E1DF + #2D2928 + #5E4137 + #FFDBCF + #200B05 + #E7BDB0 + #4A3026 + #F5DED6 + #190F0B + #D8C2BB + #41332E + #ECE3BB + #151100 + #CFC7A1 + #3B361B + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 + #FFF9F8 + #000000 + #EBC2B4 + #000000 + #FFF9F8 + #000000 + #DCC6BF + #000000 + #FFFAF4 + #000000 + #D4CBA5 + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #161312 + #E9E1DF + #161312 + #FFFFFF + #504440 + #FFF9F8 + #D8C7C2 + #D8C7C2 + #000000 + #E9E1DF + #000000 + #3D241B + #FFE0D7 + #000000 + #EBC2B4 + #261009 + #F9E2DB + #000000 + #DCC6BF + #1F1410 + #F1E7C0 + #000000 + #D4CBA5 + #1A1602 + #161312 + #3C3837 + #100E0D + #1E1B1A + #221F1E + #2D2928 + #383433 + + #C7C6C6 + #303031 + #6C6C6C + #FFFFFF + #C8C6C5 + #303030 + #3E3D3D + #D3D0D0 + #CBC5C7 + #323031 + #6F6B6D + #FFFFFF + #FFB4AB + #690005 + #93000A + #FFDAD6 + #141313 + #E5E2E1 + #141313 + #E5E2E1 + #444748 + #C4C7C7 + #8E9192 + #444748 + #000000 + #E5E2E1 + #313030 + #5E5E5E + #E3E2E2 + #1B1C1C + #C7C6C6 + #464747 + #E5E2E1 + #1B1C1C + #C8C6C5 + #474746 + #E7E1E3 + #1D1B1D + #CBC5C7 + #494648 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 + #CBCACA + #151617 + #919090 + #000000 + #CCCACA + #161616 + #929090 + #000000 + #CFC9CB + #181617 + #948F91 + #000000 + #FFBAB1 + #370001 + #FF5449 + #000000 + #141313 + #E5E2E1 + #141313 + #FEFAF9 + #444748 + #C8CBCC + #A0A3A4 + #808484 + #000000 + #E5E2E1 + #2B2A2A + #474848 + #E3E2E2 + #101111 + #C7C6C6 + #353636 + #E5E2E1 + #111111 + #C8C6C5 + #363636 + #E7E1E3 + #121112 + #CBC5C7 + #383537 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 + #FCFAFA + #000000 + #CBCACA + #000000 + #FDFAF9 + #000000 + #CCCACA + #000000 + #FFF9FA + #000000 + #CFC9CB + #000000 + #FFF9F9 + #000000 + #FFBAB1 + #000000 + #141313 + #E5E2E1 + #141313 + #FFFFFF + #444748 + #F9FBFC + #C8CBCC + #C8CBCC + #000000 + #E5E2E1 + #000000 + #292A2A + #E8E6E6 + #000000 + #CBCACA + #151617 + #E9E6E6 + #000000 + #CCCACA + #161616 + #EBE5E7 + #000000 + #CFC9CB + #181617 + #141313 + #3A3939 + #0E0E0E + #1C1B1B + #201F1F + #2A2A2A + #353434 + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles_ui.xml b/app/src/main/res/values-night/styles_ui.xml new file mode 100644 index 000000000..83b8900f6 --- /dev/null +++ b/app/src/main/res/values-night/styles_ui.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7f3ea75d0..6d668957c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -3,79 +3,76 @@ Een eenvoudige, rationele muziekspeler voor Android. - Opnieuw proberen - Toestaan + Probeer opnieuw + Sta toe Genres Artiesten Albums Nummers Alle nummers - Zoeken + Zoek Filter Alles - Sorteren + Sorteer Oplopend - Afspelen + Speel af Shuffle Speel van alle nummers - Speel af van album + Speel vanaf album Speel van artiest Nu afspelen Wachtrij - Afspelen als volgende - Toevoegen aan wachtrij - Toegevoegd aan de wachtrij + Speel volgende + Voeg toe aan wachtrij + Toegevoegd aan wachtrij Ga naar artiest Ga naar album - Staat gered - Toevoegen - Opslaan - Geen mappen + Voeg toe + Bewaar + Geen mappen Over Versie Broncode Licenties - Ontwikkeld door Alexander Capehart + Alexander Capehart Instellingen - Uiterlijk en gevoel + Uiterlijk Thema Automatisch Licht Donker - Accent + Kleuren schema Scherm - Gebruikt een afternatief notification action + Aangepaste meldingsactie Audio - Gedrag + Personaliseer Bij het afspelen vanuit de bibliotheek Onthoud shuffle Houd shuffle aan bij het afspelen van een nieuw nummer - Terugspoelen voordat je terugspoelt - Terugspoelen voordat u naar het vorige nummer gaat + Terugspoelen voor het overslaan + Terugspoelen voor het overslaan van het vorige nummer Inhoud - Afspeelstatus opslaan - Sla de huidige afspeelstatus nu op Geen muziek aangetroffen Laden van muziek mislukt Auxio heeft toestemming nodig om uw muziekbibliotheek te lezen Geen app gevonden die deze taak kan uitvoeren - Deze map wordt niet ondersteund + Deze map wordt niet ondersteund Zoek in uw bibliotheek… Nummer %d Afspelen of pauzeren - Naar volgend nummer gaan - Naar het laatste nummer gaan - Herhaalfunctie wijzigen - Zoekopdracht wissen - Map verwijderen - Auxio pictogram + Ga naar volgend nummer + Ga naar laatste nummer + Wijzig herhaalmodus + Wis zoekopdracht + Verwijder map + Auxio icoon Albumhoes voor %s - Artiesten-afbeelding voor %s - Genre-afbeelding voor %s + Artiest afbeelding voor %s + Genre afbeelding voor %s Onbekend genre Geen datum @@ -83,24 +80,24 @@ Rood Roze Paars - Dieppaars + Diep paars Indigoblauw Blauw - Diepblauw + Diep blauw Blauwgroen Groen - Diepgroen + Diep groen Cyaan Geelgroen Geel Oranje Bruin - Grijis + Grijs Nummers geladen: %d - %d lied - %d liedjes + %d nummer + %d nummers %d album @@ -108,95 +105,84 @@ Onbekende artiest Zwart thema - Gebruik een puur-zwart donker thema + Gebruik een puur zwart thema Pauze op herhaling Schijf %d %d kbps +%.1f dB - Muziekweergave bekijken en regelen + Bekijk en regel muziekweergave %d Hz Bekijk eigenschappen Naam Artiest - Annuleren + Annuleer Bibliotheek tabbladen - Jaar - Lied eigenschappen + Datum + Nummer eigenschappen Voorkeur album als er een speelt - Voorkeur titel + Voorkeur track Voorkeur album De voorversterker wordt toegepast op de bestaande afstelling tijdens weergave - Muziek mappen + Muziek mappen ReplayGain voorversterker - Modus - Bepaal waar muziek vandaan moet worden geladen + Beheer waarvan muziek moet worden geladen Totale duur: %s - Alles schudden - Oké - Altijd beginnen met spelen als een headset is aangesloten (werkt mogelijk niet op alle apparaten) + Shuffle alles + OK + Start altijd met afspelen wanneer een headset is aangesloten (werkt mogelijk niet op alle apparaten) Schakel shuffle aan of uit - De muziekbibliotheek opnieuw laden, indien mogelijk met behulp van tags uit het cachegeheugen + Laad de muziekbibliotheek opnieuw, indien mogelijk met behulp van gecashte tags Uw muziekbibliotheek wordt geladen… (%1$d/%2$d) - Uitgezonderd - Alle liedjes shuffelen - Includeer - Pauze wanneer een liedje wordt herhaald - Muziek zal niet worden geladen vanuit de mappen die u toevoegt. - Muziek verfrissen - Muziek zal alleen worden geladen uit de mappen die u toevoegt. + Shuffle alle nummers + Pauze wanneer een nummer wordt herhaald + Muziek opnieuw laden Aanpassing met tags Aanpassing zonder tags - Er speelt geen muziek + Geen muziek wordt afgespeeld Bij het afspelen van item details Ronde modus - Afgeronde hoeken inschakelen voor extra UI-elementen (vereist dat albumhoezen zijn afgerond) - Staat gerestaureerd - Bibliotheekstatistieken - Verander de zichtbaarheid en volgorde van bibliotheek-tabbladen + Schakel afgeronde hoeken in voor extra UI-elementen (vereist dat albumhoezen zijn afgerond) + Bibliotheek statistieken + Verander de zichtbaarheid en volgorde van bibliotheek tabbladen Headset automatisch afspelen - ReplayGain + ReplayGain strategie Waarschuwing: Als u de voorversterker op een hoge positieve waarde zet, kan dit bij sommige audiotracks tot pieken leiden. - Afspelen vanaf getoond item - Afspeelstatus herstellen - Herstel de eerder opgeslagen afspeelstatus (indien aanwezig) - Kan status niet herstellen - Verwijder dit wachtrij liedje - Verplaats dit wachtrij liedje - Verplaats deze tab - Album cover + Speel vanaf getoond item + Verwijder dit nummer + Verplaats dit nummer + Verplaats dit tabblad + Albumhoes Geen nummer -%.1f dB Dynamisch MPEG-1 audio - MPEG-4-audio + MPEG-4 audio Ogg audio - Matroska-audio + Matroska audio Albums geladen: %d Artiesten geladen: %d Genres geladen: %d - Muziek aan het laden + Muziek laden Album - Looptijd - Aantal Liedjes + Duur + Aantal nummers Disc Titel Formaat Grootte - Bitsnelheid - Bemonsteringsfrequentie + Bit rate + Sample rate Shuffle - Geavanceerde audio codering (GAC) - Gratis verliesvrije audiocodec (GVAC) + Geavanceerde audio codering (AAC) + Free Lossless Audio Codec (FLAC) Nieuwe afspeellijst - Afspeelstatus gewist - Geluids- en afspeelgedrag configureren - Een nieuwe afspeellijst maken - %d Geselecteerd - Toevoegen aan afspeellijst + Configureer geluid en afspeel gedrag + %d geselecteerd + Voeg toe aan afspeellijst Afspeellijst gemaakt Toegevoegd aan afspeellijst - Het thema en de kleuren van de app wijzigen - UI-besturingselementen en gedrag aanpassen + Wijzig het thema en de kleuren van de app + Pas UI elementen en gedrag aan Bepaal hoe muziek en afbeeldingen worden geladen Muziek Automatisch herladen @@ -206,8 +192,8 @@ Waarschuwing: Het gebruik van deze instelling kan ertoe leiden dat sommige tags verkeerd geïnterpreteerd worden als tags met meerdere waarden. U kunt dit oplossen door ongewenste scheidingstekens vooraf te laten gaan door een backslash (\\). Toon alleen artiesten die rechtstreeks op een album worden genoemd (werkt het beste op goed getagde bibliotheken) Sorteer namen die beginnen met cijfers of woorden zoals \"de\" correct (werkt het beste met Engelstalige muziek) - Stop met afspelen - Uw muziekbibliotheek wordt geladen… + Stop afspelen + Uw muziekbibliotheek laden… Gedrag Remix compilatie Soundtrack @@ -215,15 +201,12 @@ DJ-mix Remixen Schuine streep (/) - WeergaveWinst - Volharding + Volume normalisatie Afspeellijst Wiki - Kan status niet opslaan - Resetten + Reset Afbeeldingen - Afspeelstatus wissen - Snel + Snel Bibliotheek Live EP Remix EP @@ -232,22 +215,21 @@ Compilaties Compilatie Live - Afspeellijst hernoemen - Afspeellijst verwijderen\? + Hernoem afspeellijst + Verwijder afspeellijst? Herhaalmodus - Mappen - De wachtrij openen + Mappen + Open de wachtrij %s verwijderen\? Dit kan niet ongedaan worden gemaakt. Uit - Hoge kwaliteit + Hoge kwaliteit Genre Ampersand (&) - Bewerken + Bewerk Aflopend - Kan status niet wissen - Afspeellijst-afbeelding voor %s + Afspeellijst afbeelding voor %s Geen nummers - Gelijkmaker + Equalizer Singles Single EP\'s @@ -262,11 +244,11 @@ DJ-mixen Uw muziekbibliotheek controleren op wijzigingen… Afspeellijst hernoemd - Hernoemen - Aangepaste afspeelbalkactie - Tekens configureren die meerdere tagwaarden aanduiden - Verwijderen - Scheiders met meerdere waarden + Hernoem + Aangepaste afspeelbalk actie + Configureer tekens die meerdere tag waarden aanduiden + Verwijder + Meerdere waarden scheidingstekens Verberg bijdragers Speel vanuit genre Datum toegevoegd @@ -279,17 +261,56 @@ Intelligent sorteren Verschijnt op Afspeellijsten - Delen + Deel Afspeellijst verwijderd Naar volgende Laad de muziekbibliotheek opnieuw wanneer deze wordt gewijzigd (vereist permanente melding) - Niet-muziek uitsluiten + Sluit niet-muziek uit Negeer audiobestanden die geen muziek zijn, zoals podcasts Albumhoezen - Afspeel - Muziek opnieuw scannen - De tag-cache wissen en de muziekbibliotheek volledig opnieuw laden (langzamer, maar vollediger) - De eerder opgeslagen afspeelstatus wissen (indien aanwezig) + Afspelen + Scan muziek opnieuw + Wis de tag-cache en laad de muziekbibliotheek opnieuw (langzamer, maar vollediger) Geen schijf - %s aan het bewerken + %s bewerken + Uit + Kan de afspeellijst niet naar dit bestand exporteren + Geen albums + Nummer + Geïmporteerde afspeellijst + Afbeelding selectie + Demo + Demo\'s + Sorteer op + Meer + Richting + ReplayGain spoor aanpassing + ReplayGain album aanpassing + Auteur + Doneer + Supporters + Doneer aan het project om uw naam hier te krijgen! + Forceer vierkante albumhoezen + Snijd alle albumhoezen bij tot een verhouding van 1:1 + Fout informatie + Gekopieerd + Bekijk + Selectie + Onthoud pauze + Blijf afspelen/pauzeren tijdens het overslaan of bewerken van wachtrijen + Lege afspeellijst + Importeer afspeellijst + Importeer + Absoluut + Kan geen afspeellijst uit dit bestand importeren + Relatief + Afspeellijst geïmporteerd + Speel nummer zelf af + Exporteer afspeellijst + Exporteer + Pad + Pad stijl + Gebruik Windows-compatibele paden + Rapporteer + Afspeellijst geëxporteerd \ No newline at end of file diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml new file mode 100644 index 000000000..108229ec8 --- /dev/null +++ b/app/src/main/res/values-nn/strings.xml @@ -0,0 +1,314 @@ + + + Spor + Sorter + Stigande + Legg til i speleliste + Retning + Legg til i kø + Bland + Syn album + Del + Bane + Lisensar + Kopiert + Bland alle + OK + Begynn avspeling + Legg til + Absolutt + Lagre + Kjeldekode + Wiki + Sporeigenskapar + Ein enkel, rasjonell musikkspillar for Android. + Musikk vert lasta inn + Held auge med musikkbiblioteket + Løyv + Spor + Spor + Alle spor + Album + Album + Remiks-album + EP + Live-EP + Remiks-EP + Singlar + Singel + Samanstilling + Live-samanstilling + Remiks-samanstilling + Ljodspor + Miksteipar + Miksteip + DJ-miksar + DJ-miks + Live + Remiksar + Opptrer på + Artist + Artistar + Sjanger + Sjangrar + Ny speleliste + Tom speleliste + Importert speleliste + Importer + Eksporter speleliste + Slett + Slett speleliste? + Rediger + Lengd + Sporantal + Platenummer + Dato tillagt + Spelar no + Tonekontroll + Spel + + Spel neste + Syn artist + Syn eigenskapar + Syn + Bitrate + Samplingsfrekvens + ReplayGain-sporjustering + ReplayGain-albumjustering + Bland + Avbryt + Tilbakestill + Meir + Banestil + Relativt + Bruk Windows-kompatible baner + Om + Utgåve + Bibliotekstatistikk + Rapporter + Utvikla av Alexander Capehart + Lastar inn musikk + Prøv igjen + Syn meir + Live-singel + Ljodspor + Live-album + EP + Remiks-singel + Samanstillingar + Demoar + Demo + Format + Storleik + Spelelister + Bytt namn + Importer speleliste + Eksporter + Søk + Namn + Dato + Speleliste + Bytt namn på speleliste + Filtrer + Alle + Sorter etter + Fallande + Utviklar + Utval + Feilopplysningar + La til i speleliste + Albumomslag for %s + Opprett ei sak på GitHub + Send ein e-post + Donér + Støttespelarar + Syn og kontroller musikkavspeling + Held auge med endringar i musikkbiblioteket ditt… + La til i køen + Speleliste importert + Namn på speleliste endra + Speleliste eksportert + Spelelista vart sletta + Donér til prosjektet, så skal namnet ditt leggjast til her! + Søk igjennom biblioteket ditt… + Utsjånad + Drakt + Automatisk + Fargedrakt + Svart drakt + Bruk ein heilsvart mørk drakt + Avrunda hjørne + Slå på avrunda hjørne på fleire grensesnittselement (krev at albumomslag er òg avrunda) + Skjerm + Bibliotekfaner + Hopp til neste + Gjentakingsmodus + Åtferd + Ved avspeling frå biblioteket + Spel frå album + Spel frå sjanger + Spel sporet for seg sjølv + Hugs blandingsmodus + Innhald + Musikk + Automatisk gjeninnlasting + Utelat ikkje-musikk + Fleirverdi-inndelarar + Still inn teikn for inndeling av fleire verdiar på etiketten + Komma (,) + Intelligent sortering + Sorterer namn som byrjar med siffer eller ord som «the» korrekt (fungerer best med engelskspråkig musikk) + Døl medverkande + Syn kun artistar som er direkte kreditert på albumet (fungerer best med velmerka bibliotek) + Bilete + Albumomslag + Av + Høg kvalitet + Raskt + Påtving firkanta albumomslag + Beskjer alle albumomslag til 1:1-sideforhold + Still inn ljod- og avspelingsåtferd + Avspeling + Automatisk avspeling med headsett + Spol att før spor vert hoppa over + Spol att før Auxio hoppar til forrige spor + Pause ved gjentaking + Hugs pausemodus + Vert spelande/pausa ved sporbytting eller køredigering + Volumnormalisering + ReplayGain-strategi + Av + Føretrekk spor + Føretrekk album + Føretrekk album om eit album vert avspelt + ReplayGain-forforsterkning + Forforsterkning vert bruka på justeringa allereie teken i bruk under avspeling + Justering med etikettar + Justering utan etikettar + Bibliotek + Handsam kvar musikk skal lastast inn ifrå + Mapper + Gjenoppfrisk musikk + Kan ikkje importere ei speleliste frå denne fila + Hopp til neste spor + Endre gjentakingsmodus + Slå på/av blanding + Opne køen + Albumomslag + Spelelistebilete for %s + Inkje spor + Ingen spor + Ingen album + Fri tapsfri ljodkodek (FLAC) + Raud + Rosa + Lilla + Mørklilla + Mørkblå + Cyanblå + %1$s, %2$s + Plate %d + Speleliste %d + Lastar inn musikkbiblioteket ditt… (%1$d/%2$d) + Slett %s for godt? Dette kan ikkje angrast. + Innlasta spor: %d + Innlasta sjangrar: %d + + %d spor + %d spor + + + %d album + %d album + + + %d artist + %d artistar + + Gje tilbakemelding + Lastar inn musikkbiblioteket ditt… + Speleliste oppretta + Begynner Auxio med den forrige lagra tilstanden. Dersom ingen lagra tilstand er tilgjengeleg, skal alle spor blandast. Avspeling begynner med éin gong.\n\nÅTVARING: Ver varsom med å kontrollere denne tenesten, viss du lukkar han og så prøver å bruke han igjen, skal det nok få appen til å krasje. + Innstillingar + Endre drakten og programfargane + Ljos + Mørk + Tilpass + Tilpass grensesnittskontrollar og åtferd + Eigendefinert avspelingsfelthandling + Eigendefinert merknadshandling + Endre synlegdom og rekkjefylgje på bibliotekfaner + Spel frå synt element + Ved avspeling frå elementsdetaljar + Spel frå alle spor + Spel frå artist + Kontroller korleis musikk og bilete vert innlasta + Last inn musikkbiblioteket på nytt når det vert endra (krev vedvarande merknad) + Behald blanding når eit nytt spor vert spelt + Ignorer ljodfiler som ikkje er musikk, som t.d. podkastar + Semikolon (;) + Skråstrek (/) + Plussteikn (+) + Ampersand (&) + Åtvaring: Viss denne innstillinga er påslått, kan enkelte etikettar verte feiltolka som om dei har fleire verdiar. Dette kan løysast ved å leggje ein omvend skråstrek (\\) bak skiljeteiknet som ikkje skal skilja verdiar. + Ljod + Alltid byrj avspeling når hovudtelefonar vert tilkopla (verkar kanskje ikkje på alle einingar) + Pause når eit spor vert gjenteke + Åtvaring: Å endre forforsterkninga til høge positive verdier, kan forårsake forvrengning ved høge ljodtrykk på nokre spor. + Musikkmapper + Last inn musikkbiblioteket på nytt og bruk hurtiglagra etikettar når mogleg + Fann ikkje musikk + Skann musikk på nytt + Tøm etiketthurtiglager og last inn heile musikkbiblioteket på nytt (tregare, men meir fullstendig) + Auxio treng løyve til å lesa musikkbiblioteket ditt + Artistbilete for %s + Klarte ikkje å laste inn musikk + Kan ikkje eksportere spelelista til denne fila + Denne mappa er ikkje støtta + Ingen app funne som kan handsama denne oppgåva + Spel av eller pause + Auxio-ikon + Spor %d + Ingen mapper + Bland alle spor + Fjern dette sporet + Flytt dette sporet + Hopp til siste spor + Stopp avspeling + Flytt denne fana + Tøm søket + Ukjend sjanger + Fjern mappe + Utvalsbilete + Ingen dato + +%.1f dB + Sjangerbilete for %s + Ukjend artist + OGG-ljod + Limegrønn + Gul + Oransje + Inga plate + Matroska-ljod + Ingen musik vert spelt av + MPEG-4-ljod + Avansert audio-koding (AAC) + Blå + Grønn + MPEG-1-ljod + Indigo + Blågrønn + Mørkgrønn + Grå + Redigerer %s + Brun + Dynamisk + %d valde + %d kbps + Innlasta artistar: %d + -%.1f dB + %d Hz + Innlasta album: %d + Total lengd: %s + Vel mapper + diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 9e23fb78b..87feed829 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -63,8 +63,6 @@ ਰੱਦ ਕਰੋ ਸਾਂਭੋ ਰੀਸੈਟ ਕਰੋ - ਸਟੇਟ ਕਲੀਅਰ ਕੀਤੀ ਗਈ - ਸਟੇਟ ਰੀਸਟੋਰ ਕੀਤੀ ਗਈ ਦੇ ਬਾਰੇ ਸੰਸਕਰਣ ਸ੍ਰੋਤ ਕੋਡ @@ -78,7 +76,6 @@ ਬਿੱਟ ਰੇਟ ਸੈਂਪਲ ਰੇਟ ਸ਼ਾਮਿਲ ਕਰੋ - ਸਟੇਟ ਸਾਂਭੀ ਗਈ ਤਬਦੀਲੀਆਂ ਲਈ ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਦੀ ਨਿਗਰਾਨੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… ਲਾਈਸੈਂਸ ਸੰਗੀਤ ਪਲੇਬੈਕ ਵੇਖੋ ਅਤੇ ਕੰਟਰੋਲ ਕਰੋ @@ -117,26 +114,17 @@ ਸ਼ੈਲੀ ਤੋਂ ਖੇਡੋ ਸ਼ਫਲ ਯਾਦ ਰੱਖੋ ਗੀਤ ਦੁਹਰਾਉਣ ਤੇ ਰੋਕੋ - ਰੀਪਲੇਅ-ਗੇਨ + ਵਾਲੀਅਮ ਨਾਰਮਲਾਈਜ਼ੇਸ਼ਨ ਰੀਪਲੇਅ-ਗੇਨ ਰਣਨੀਤੀ ਟਰੈਕ ਨੂੰ ਤਰਜੀਹ ਐਲਬਮ ਨੂੰ ਤਰਜੀਹ ਬਿਨਾਂ ਟੈਗਾਂ ਦੇ ਐਡਜਸਟਮੈਂਟ - ਪ੍ਰਬੰਧਿਤ ਕਰੋ ਕਿ ਸੰਗੀਤ ਕਿੱਥੋਂ ਲੋਡ ਕੀਤਾ ਜਾਣਾ ਚਾਹੀਦਾ ਹੈ - ਫੋਲਡਰ - ਬਾਹਰ ਰੱਖੋ - ਸ਼ਾਮਿਲ ਕਰੋ + ਪ੍ਰਬੰਧਿਤ ਕਰੋ ਕਿ ਸੰਗੀਤ ਕਿੱਥੋਂ ਲੋਡ ਕੀਤਾ ਜਾਣਾ ਚਾਹੀਦਾ ਹੈ + ਫੋਲਡਰ ਸੰਗੀਤ ਤਾਜ਼ਾ-ਤਰੀਨ ਕਰੋ - ਪਰਸਿਸਟੈਂਟ - ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰੋ - ਮੌਜੂਦਾ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਹੁਣੇ ਸੁਰੱਖਿਅਤ ਕਰੋ - ਪਲੇਬੈਕ ਸਥਿਤੀ ਸਾਫ਼ ਕਰੋ - ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰੋ - ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰੋ (ਜੇ ਕੋਈ ਹੈ) ਕੋਈ ਐਪ ਨਹੀਂ ਮਿਲੀ ਜੋ ਇਸ ਕਾਰਜ ਨੂੰ ਸੰਭਾਲ ਸਕਦੀ ਹੈ - ਕੋਈ ਫੋਲਡਰ ਨਹੀਂ - ਇਹ ਫੋਲਡਰ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ - ਸਥਿਤੀ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ + ਕੋਈ ਫੋਲਡਰ ਨਹੀਂ + ਇਹ ਫੋਲਡਰ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ ਚਲਾਓ ਜਾਂ ਰੋਕੋ ਅਗਲੇ ਗੀਤ \'ਤੇ ਜਾਓ ਆਖਰੀ ਗੀਤ \'ਤੇ ਜਾਓ @@ -148,16 +136,10 @@ ਪ੍ਰੀ-ਐਂਪ ਨੂੰ ਪਲੇਬੈਕ ਦੌਰਾਨ ਮੌਜੂਦਾ ਵਿਵਸਥਾ \'ਤੇ ਲਾਗੂ ਕੀਤਾ ਜਾਂਦਾ ਹੈ ਟੈਗਸ ਨਾਲ ਐਡਜਸਟਮੈਂਟ ਚੇਤਾਵਨੀ: ਪ੍ਰੀ-ਐਂਪ ਨੂੰ ਉੱਚ ਸਕਾਰਾਤਮਕ ਮੁੱਲ ਵਿੱਚ ਬਦਲਣ ਦੇ ਨਤੀਜੇ ਵਜੋਂ ਕੁਝ ਆਡੀਓ ਟਰੈਕਾਂ \'ਤੇ ਸਿਖਰ ਹੋ ਸਕਦਾ ਹੈ। - ਤੁਹਾਡੇ ਦੁਆਰਾ ਸ਼ਾਮਲ ਕੀਤੇ ਫੋਲਡਰਾਂ ਤੋਂ ਸੰਗੀਤ ਨੂੰ ਲੋਡ ਕੀਤਾ ਨਹੀਂ ਜਾਵੇਗਾ। - ਤੁਹਾਡੇ ਦੁਆਰਾ ਸ਼ਾਮਲ ਕੀਤੇ ਫੋਲਡਰਾਂ ਤੋਂ ਸੰਗੀਤ ਸਿਰਫ਼ ਲੋਡ ਕੀਤਾ ਜਾਵੇਗਾ। - ਮੋਡ ਟੈਗ ਕੈਸ਼ ਨੂੰ ਸਾਫ਼ ਕਰੋ ਅਤੇ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਰੀਲੋਡ ਕਰੋ (ਹੌਲੀ, ਪਰ ਵਧੇਰੇ ਸੰਪੂਰਨ) - ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਪਲੇਬੈਕ ਸਥਿਤੀ ਨੂੰ ਸਾਫ਼ ਕਰੋ (ਜੇ ਕੋਈ ਹੈ) ਕੋਈ ਸੰਗੀਤ ਨਹੀਂ ਮਿਲਿਆ ਸੰਗੀਤ ਲੋਡ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ Auxio ਨੂੰ ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਪੜ੍ਹਨ ਲਈ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ - ਸਥਿਤੀ ਨੂੰ ਰੀਸਟੋਰ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ - ਸਥਿਤੀ ਨੂੰ ਸਾਫ਼ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ %d ਨੂੰ ਟਰੈਕ ਕਰੋ ਸਿਰਫ਼ ਉਹਨਾਂ ਕਲਾਕਾਰਾਂ ਨੂੰ ਦਿਖਾਓ ਜੋ ਕਿਸੇ ਐਲਬਮ \'ਤੇ ਸਿੱਧੇ ਤੌਰ \'ਤੇ ਕ੍ਰੈਡਿਟ ਕੀਤੇ ਜਾਂਦੇ ਹਨ (ਚੰਗੀ ਤਰ੍ਹਾਂ ਨਾਲ ਟੈਗ ਕੀਤੀਆਂ ਲਾਇਬ੍ਰੇਰੀਆਂ \'ਤੇ ਵਧੀਆ ਕੰਮ ਕਰਦਾ ਹੈ ਉਹਨਾਂ ਆਡੀਓ ਫਾਈਲਾਂ ਨੂੰ ਅਣਡਿੱਠ ਕਰੋ ਜੋ ਸੰਗੀਤ ਨਹੀਂ ਹਨ, ਜਿਵੇਂ ਕਿ ਪੌਡਕਾਸਟ @@ -184,13 +166,13 @@ ਚਿੱਤਰ ਐਲਬਮ ਕਵਰ ਬੰਦ - ਤੇਜ - ਉੱਚ ਕੁਆਲਿਟੀ + ਤੇਜ + ਉੱਚ ਕੁਆਲਿਟੀ ਆਡੀਓ ਪਿਛਲੇ ਗੀਤ \'ਤੇ ਜਾਣ ਤੋਂ ਪਹਿਲਾਂ ਰੀਵਾਈਂਡ ਕਰੋ ਦੁਹਰਾਉਣ \'ਤੇ ਰੁਕੋ ਲਾਇਬ੍ਰੇਰੀ - ਸੰਗੀਤ ਫੋਲਡਰ + ਸੰਗੀਤ ਫੋਲਡਰ ਕਤਾਰ ਖੋਲ੍ਹੋ ਇਸ ਕਤਾਰ ਗੀਤ ਨੂੰ ਹਟਾਓ ਇਸ ਕਤਾਰ ਗੀਤ ਨੂੰ ਮੂਵ ਕਰੋ @@ -208,7 +190,6 @@ ਪਲੇਅ-ਲਿਸਟ ਦਾ ਨਾਂ ਬਦਲਿਆ ਨੀਲਾ-ਹਰਾ ਮਿਟਾਓ - ਇੱਕ ਨਵੀਂ ਪਲੇਅ-ਲਿਸਟ ਬਣਾਓ ਪਲੇਅ- ਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕੀਤਾ ਇਹ ਟੈਬ ਹਿਲਾਓ ਕੋਈ ਗੀਤ ਨਹੀਂ @@ -273,7 +254,7 @@ ਸਲੇਟੀ ਕੁੱਲ ਮਿਆਦ: %s - ਫੋਲਡਰ ਹਟਾਓ + ਫੋਲਡਰ ਹਟਾਓ Auxio ਆਈਕਾਨ ਉੱਤੇ ਵਿਖਾਈ ਦਿੰਦਾ ਹੈ ਪਲੇਅ-ਲਿਸਟਾਂ @@ -322,4 +303,9 @@ ਆਪਣਾ ਨਾਮ ਇੱਥੇ ਜੋੜਨ ਲਈ ਪ੍ਰੋਜੈਕਟ ਨੂੰ ਦਾਨ ਕਰੋ! ਕਤਾਰ ਨੂੰ ਛੱਡਣ ਜਾਂ ਸੰਪਾਦਿਤ ਕਰਨ ਵੇਲੇ ਚਲਾਉਂਦੇ/ਰੋਕੇ ਰਹੋ ਵਿਰਾਮ ਯਾਦ ਰੱਖੋ + ਬੰਦ + ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਕਰੋ + ਪਹਿਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਸਥਿਤੀ ਦੀ ਵਰਤੋਂ ਕਰਕੇ Auxio ਨੂੰ ਸ਼ੁਰੂ ਕਰਦਾ ਹੈ। ਜੇਕਰ ਕੋਈ ਰੱਖਿਅਤ ਸਥਿਤੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ, ਤਾਂ ਸਾਰੇ ਗੀਤ ਸ਼ਫਲ ਤੇ ਚੱਲਣਗੇ ਅਤੇ ਪਲੇਬੈਕ ਤੁਰੰਤ ਸ਼ੁਰੂ ਹੋ ਜਾਵੇਗਾ। +\n +\nਚੇਤਾਵਨੀ: ਇਸ ਸੇਵਾ ਨੂੰ ਨਿਯੰਤਰਿਤ ਕਰਨ ਵਿੱਚ ਸਾਵਧਾਨ ਰਹੋ, ਜੇਕਰ ਤੁਸੀਂ ਇਸਨੂੰ ਬੰਦ ਕਰਦੇ ਹੋ ਅਤੇ ਫਿਰ ਇਸਨੂੰ ਦੁਬਾਰਾ ਵਰਤਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਸ਼ਾਇਦ ਐਪ ਨੂੰ ਕ੍ਰੈਸ਼ ਕਰ ਦਿਓਗੇ। \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 02c6dd410..dfd3baee3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -129,7 +129,7 @@ Brak utworu Korektor Rozmiar - Brak folderów + Brak folderów Odtwórz wszystkie utwory Odtwórz album Automatycznie odtwórz muzykę po podłączeniu słuchawek (może nie działać na wszystkich urządzeniach) @@ -149,14 +149,13 @@ Przejdź do następnego utworu Autorstwa Alexandra Capeharta Zaokrąglone krawędzie - Włącz zaokrąglone rogi na dodatkowych elementach interfejsu (wymaga zaokrąglenia okładek albumów) + Włącz zaokrąglone narożniki na dodatkowych elementach interfejsu (wymaga zaokrąglenia okładek albumów) Akcja na pasku odtwarzania Następny utwór Tryb powtarzania Ustawienie ReplayGain Preferuj album, jeśli jest odtwarzany Odtwarzanie z widoku biblioteki - Zapisz stan odtwarzania Przecinek (,) Średnik (;) Ukośnik (/) @@ -165,35 +164,24 @@ MPEG-4 Ogg Prosty i praktyczny odtwarzacz muzyki na Androida. - Usuń folder + Usuń folder Pokaż kolejkę AAC Automatycznie odśwież bibliotekę po wykryciu zmian (wymaga stałego powiadomienia) - Wyklucz - Zawrzyj Zmień pozycję utworu w kolejce Przesuń kartę Wizerunek wykonawcy dla %s Ładuję bibliotekę muzyczną… Preferuj utwór - Przywrócono stan odtwarzania - Wyczyszczono stan odtwarzania - Zapisano stan odtwarzania Karty w bibliotece Zmień widoczność i kolejność kart w bibliotece Użyj czarnego motywu Elementy Material You %d kb/s - Zapisz obecny stan odtwarzania - Wyczyść stan odtwarzania Matroska - Wyczyść poprzedni stan odtwarzania (jeśli istnieje) - Przywróć stan odtwarzania - Przywróć poprzedni stan odtwarzania (jeśli istnieje) - Foldery z muzyką - Wybierz z których folderów importowane są utwory - Tryb + Foldery z muzyką + Wybierz z których folderów importowane są utwory Przewiń przed odtworzeniem poprzedniego utworu Przewiń do początku obecnie odtwarzanego utworu zamiast odtworzenia poprzedniego Preamplifier ReplayGain @@ -207,7 +195,6 @@ Użyj alternatywnej akcji w powiadomieniu Zatrzymaj odtwarzanie przy powtórzeniu Wyczyść zapytanie wyszukiwania - Nie można przywrócić stanu odtwarzania Okładka gatunku %s Podgląd i sterowanie odtwarzanianiem muzyki Regulacja w oparciu o tagi @@ -216,17 +203,15 @@ Odtwarzanie z widoku szczegółowego Odtwórz tylko wybrane Zatrzymaj odtwarzanie, kiedy utwór się powtórzy - Muzyka będzie importowana tylko z wybranych folderów. Znaki oddzielające wartości Wybierz znaki oddzielające poszczególne wartości w metadanych Auxio wymaga zgody na dostęp do twojej biblioteki muzycznej - Ten folder nie jest wspierany + Ten folder nie jest wspierany Utwory nie są odtwarzane Importuję bibliotekę muzyczną… (%1$d/%2$d) Zaimportowane albumy: %d Zaimportowane gatunki: %d Łączny czas trwania: %s - Muzyka nie będzie importowana z wybranych folderów. Zmień tryb powtarzania Odtwórz losowo wszystkie utwory Monitoruję zmiany w bibliotece muzycznej… @@ -239,9 +224,9 @@ Wyklucz inne pliki dźwiękowe Okładki albumów Wyłączone - Niska jakość - Wysoka jakość - Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów (tak, jakby posiadały wiele wartości). Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami oddzielającymi. + Niska jakość + Wysoka jakość + Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów (mogą być przetwarzane tak, jakby posiadały wiele wartości). Problem ten należy rozwiązać stawiając ukośnik wsteczny () przed niepożądanymi znakami oddzielającymi. Dostosuj motyw i kolory aplikacji Ukryj wykonawców uczestniczących Zarządzaj importowaniem muzyki i obrazów @@ -249,12 +234,11 @@ Dostosuj elementy i funkcje interfejsu Pokaż tylko artystów bezpośrednio przypisanych do albumu (działa najlepiej w przypadku dobrze otagowanych bibliotek) Odtwarzanie - Foldery - Stan odtwarzania + Foldery Obrazy Zarządzaj dźwiękiem i odtwarzaniem muzyki Wybrano %d - Wyrównanie głośności (ReplayGain) + Normalizacja głośności Resetuj Wiki Funkcje @@ -269,15 +253,12 @@ %1$s, %2$s Muzyka - Nie można wyczyścić stanu odtwarzania - Nie można zapisać stanu odtwarzania Malejąco Playlisty Playlista Obraz playlisty %s Inteligentne sortowanie - Ignoruj słowa takie jak „the” oraz numery w tytule podczas sortowania (działa najlepiej z utworami w języku angielskim) - Utwórz nową playlistę + Ignoruj słowa takie jak „the” oraz liczby w tytule podczas sortowania (działa najlepiej z utworami w języku angielskim) Nowa playlista Dodaj do playlisty Utworzono playlistę @@ -299,8 +280,59 @@ Przytnij okładki do formatu 1:1 Wymuś kwadratowe okładki Piosenka - Odtwarzanie utworu samodzielnie + Odtwórz tylko utwór Widok Sortuj według Kierunek + Brak albumów + Zaimportowane playlisty + Importuj + Zgłoś + Relatywna + Wyłącz + Informacje o błędzie + Zaimportowano playlistę + Eksportuj + Styl ścieżki + Wybór obrazu + Więcej + Zaznaczenie + Skopiowano + Autor + Wspomóż finansowo + Wspierający + Wyeksportowano playlistę + Pusta playlista + Importuj playlistę + Ścieżka + Eksportuj playlistę + Absolutna + Demo + Dema + Więcej + Uruchamia Auxio z użyciem poprzednio zapisanego stanu. Jeśli nie zapisano poprzedniego stanu, wszystkie utwory będą odtwarzane w losowej kolejności. Odtwarzanie rozpocznie się natychmiast.\n\nUWAGA: Zachowaj ostrożność przy korzystaniu z tej opcji. Jeśli ją zamkniesz, a następnie spróbujesz ponownie z niej skorzystać, aplikacja może się zawiesić. + Nie udało się wyeksportować playlisty do tego pliku + Dopasowanie głośności utworu (ReplayGain) + Dopasowanie głośności albumu (ReplayGain) + Wesprzyj projekt darowizną, aby Twoje imię pojawiło się tutaj! + Pozostań w trakcie odtwarzania/zatrzymania podczas pomijania lub edytowania kolejki + Zapamiętaj pauzę + Zacznij odtwarzanie + Nie udało się zaimportować playlisty z tego pliku + Używaj ścieżek kompatybilnych z Windows + Wyślij e-maila + Informacja zwrotna + Zgłoś problem na GitHubie + Wybierz foldery + MPEG-4 zawierający %s + Nieznany + Nieznany album + Nowy folder + Apple Lossless Audio Codec (ALAC) + Oszczędzanie miejsca + Tutaj pojawią się dodane utwory. + Tutaj pojawią się dodane albumy. + Tutaj pojawią się dodani artyści. + Tutaj pojawią się dodane playlisty. + Tutaj pojawią się dodane gatunki. \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4b659230c..4ba7c30a5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -80,24 +80,19 @@ Usar player de notificação alternativo A pré-amplificação é aplicada em cima da normalização de volume Aviso: Alterar a pré-amplificação para um valor positivo muito alto pode resultar em picos de volume em algumas faixas. - Pastas de música - Gerencia de onde as músicas devem ser carregadas - Incluir + Pastas de música + Gerencia de onde as músicas devem ser carregadas Falha ao carregar músicas Nenhum aplicativo encontrado que possa lidar com esta tarefa - Sem pastas - Esta pasta não é compatível + Sem pastas + Esta pasta não é compatível Recarrega a biblioteca de músicas usando metadados salvos em cache quando possível Retroceder antes de voltar Recarregar música Retroceder a música antes de voltar para a anterior - As músicas serão carregadas somente das pastas que você adicionar. - Excluir - As músicas não serão carregadas das pastas que você adicionar. - Modo O Auxio precisa de permissão para ler sua biblioteca de músicas Um reprodutor de música simples e racional para android. - Carregando sua biblioteca de músicas… + Carregando a sua biblioteca de músicas… Ano Duração Contagem de músicas @@ -110,7 +105,6 @@ Tamanho Taxa de bits Taxa de amostragem - Lista salva Altere a ordem e visibilidade das abas da biblioteca Tema Amoled Use um tema Amoled @@ -118,7 +112,6 @@ Deixa o modo aleatório ativo ao reproduzir uma nova música Pausar ao repetir Pausar quando uma música se repete - Salva a lista de reprodução atual Pular para a música anterior Alterar o modo de repetição Aleatorizar todas das músicas @@ -129,7 +122,7 @@ Capa do álbum Pular para a próxima música Ativa ou desativa o modo aleatório - Remover pasta + Remover pasta Ciano Roxo escuro Índigo @@ -182,28 +175,20 @@ Abas da biblioteca Gênero Reproduzir do artista - Restaura a lista de reprodução salva anteriormente (se houver) Ajuste em faixas com metadados - Lista limpa - Lista restaurada Carregando música Carregando música Monitorando a biblioteca de músicas Cantos arredondados Pular para o próximo Reproduzir do álbum - Salvar lista de reprodução - Limpar lista de reprodução - Restaurar lista de reprodução Visualize e controle a reprodução de música Ação personalizada na barra de reprodução Modo de repetição - Limpa a lista de reprodução salva anteriormente (se houver) EPs EP Singles Single - Nenhuma lista pode ser restaurada Compilações Compilação Single remix @@ -236,8 +221,8 @@ Ignorar arquivos que não sejam músicas Capas de álbuns Desligado - Rápido - Alta qualidade + Rápido + Alta qualidade Mixagens de DJ Mixagem de DJ @@ -247,8 +232,6 @@ Re-escanear músicas Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) - Não foi possível limpar a lista - Não foi possível salvar a lista Ocultar artistas colaboradores Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos) %d Selecionadas @@ -263,12 +246,11 @@ Customize os controles da interface e o comportamento Música Controle como as músicas e imagens são carregadas - Normalização de volume (ReplayGain) + Normalização de volume Biblioteca - Persistência Comportamento - Pastas - Descendente + Pastas + Decrescente Ignorar artigos ao classificar Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) Playlists @@ -287,7 +269,6 @@ Organizar por Música Apagar playlist\? - Criar uma nova playlist Playlist deletada Nova playlist Playlist renomeada @@ -314,7 +295,7 @@ Seleção Exportar Exportar playlist - Direção + Ordem Estilo de caminho Absoluto Relativo @@ -329,4 +310,28 @@ Recorta todas as capas de álbuns para uma proporção de imagem 1:1 Sem músicas Informar + Desligado + Imagem de playlist para %s + Reproduzir música sozinha + Imagem de seleção + Iniciar a reprodução + Inicia o Auxio usando o estado salvo anteriormente. Se nenhum estado salvo estiver disponível, todas as músicas serão reproduzidas aleatoriamente. +\n +\nAVISO: Tenha cuidado ao controlar este serviço. Se você fechá-lo e então tentar usá-lo novamente, provavelmente ocasionará um crash no aplicativo. + Mais + Envie um e-mail + Crie uma issue no GitHub + Escolher pastas + MPEG-4 contendo %s + Apple Lossless Audio Codec (ALAC) + Desconhecido + Álbum desconhecido + Feedback + As suas músicas aparecerão aqui. + Os seus álbuns aparecerão aqui. + Os seus artistas aparecerão aqui. + As suas playlists aparecerão aqui. + Os seus gêneros aparecerão aqui. + Salvar espaço + Nova pasta \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index d6d307c19..a71a388f6 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -89,17 +89,17 @@ Estatísticas da biblioteca Capa do álbum Data - Rápido - Qualidade alta + Rápido + Qualidade alta Personalizar a barra de reprodução Modo de repetição Reproduzir do artista Pausar na repetição O Auxio precisa de permissão para ler a sua biblioteca de músicas - Sem pastas - Esta pasta não é compatível + Sem pastas + Esta pasta não é compatível Mover esta música da fila - Remover pasta + Remover pasta Mistura de compilações Compilação ao vivo Disco @@ -120,8 +120,6 @@ Propriedades da música OK Adicionar - Estado salvo - Estado limpo Tema preto Limpar consulta de pesquisa Imagem de gênero para %s @@ -139,19 +137,14 @@ Duração Cancelar A carregar biblioteca de músicas… - Configurar onde a música deve ser carregada + Configurar onde a música deve ser carregada Gênero Mantenha a reprodução aleatória ao reproduzir uma nova música Pular para a próxima música Aviso: Usar essa configuração pode resultar em algumas etiquetas serem interpretadas incorretamente como tendo múltiplos valores. Pode resolver isso pré-definindo caracteres de separador indesejados com uma barra invertida (\\). Áudio MPEG-1 Recarregamento automático - Pastas de música - Modo - Excluir - A música não será carregada das pastas que adicionar. - Incluir - A música será somente carregada das pastas que adicionar. + Pastas de música Excluir não-música Ignorar ficheiros de áudio que não são música, tal como podcasts Configurar caracteres que denotam múltiplos valores de etiqueta @@ -174,7 +167,6 @@ A monitorizar a biblioteca de música Equalizador Um reprodutor de música simples e racional para Android. - Estado restaurado Mostrar Abas da biblioteca Altere a visibilidade e a ordem das abas da biblioteca @@ -189,9 +181,6 @@ O pré-amplificador é aplicado ao ajuste existente durante a reprodução Reproduzir de todas as músicas Pausar quando uma música é repetida - Limpar o estado de reprodução salvo anteriormente (se houver) - Restaurar o estado de reprodução - Restaurar o estado de reprodução salvo anteriormente (se houver) Ativar ou desativar a reprodução aleatória Embaralhar todas as músicas Remover esta música de fila @@ -206,9 +195,6 @@ Mixtape Remixes Artista - Gravar estado da reprodução - Salvar o estado de reprodução atual - Limpar estado de reprodução Álbum ao vivo -%.1f dB %d kbps @@ -219,7 +205,7 @@ Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) %d Selecionadas Misturas DJ - DJ Mix + Mixagem de DJ Aleatório Ocultar colaboradores Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) @@ -240,10 +226,7 @@ Retrocede a música antes de voltar para a anterior Recarregar música %1$s, %2$s - Não foi possível limpar a lista - Não foi possível gravar a lista Procurar músicas novamente - Nenhuma lista pode ser restaurada Ícone do Auxio Misturar tudo Ao tocar da biblioteca @@ -255,7 +238,7 @@ %d artistas %d artistas - Configurar ganho de repetição + Normalização de volume Descendente Mudar o tema e cores da app Personalize os controlos e o comportamento do interface do utilizador @@ -264,9 +247,8 @@ Imagens Configurar o som e comportamento da reprodução Reprodução - Pastas + Pastas Biblioteca - Estado da reprodução E comercial (&) Comportamento Classificação inteligente @@ -295,8 +277,7 @@ Ordenar por Visualizar Música - Eliminar lista de reprodução - Criar nova lista de reprodução + Apagar lista de reprodução? Lista de reprodução eliminada Relatório Nova lista de reprodução @@ -306,4 +287,33 @@ Renomear lista de reprodução Excluir %s\? Não pode ser desfeito. Só aparecer + Desligado + Incapaz de exportar a lista de reprodução para este ficheiro + Lista de reprodução importada + Não foi possível importar uma lista de reprodução deste ficheiro + Demo + Demos + Ajuste de ReplayGain da faixa + Autor + Doar + Apoiadores + Lista de reprodução exportada + Doe para o projeto para ter o seu nome adicionado aqui! + Lembrar pausa + Continuar a reproduzir/pausado quando ao pular ou editar a fila + Lista de reprodução vazia + Importar lista de reprodução + Sem álbuns + Caminho + Importar + Exportar + Exportar lista de reprodução + Estilo de caminho + Absoluto + Lista de reprodução importada + Ajuste de ReplayGain do álbum + Relativo + Usar caminhos compatíveis com Windows + Inicia o Auxio utilizando o estado anteriormente guardado. Se não existir nenhum estado guardado, todas as músicas serão reproduzidas aleatoriamente. A reprodução começará imediatamente.\n\nAVISO: Tenha cuidado ao controlar este serviço. Se o fechar e tentar utilizá-lo novamente, provavelmente irá causar uma falha na aplicação. + Iniciar reprodução \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3a0906840..f193cf386 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,2 +1,328 @@ - \ No newline at end of file + + Lista de reprodução importada + A monitorizar a biblioteca de música + Tentar novamente + Mais + Um reprodutor de música simples e racional para Android. + Permitir + Música + Todas as músicas + Álbuns + Álbum + Compilações + Importar + Gênero + Lista de reprodução importada + Géneros + Reproduzir + Reproduzir a próxima + Adicionar à fila + Adicionar à lista de reprodução + Propriedades da música + Formato + Fila + Repor + Código fonte + Seleção + A carregar biblioteca de músicas… + Licenças + Estatísticas da biblioteca + Artistas + Lista de reprodução + Listas de reprodução + Adicionada à fila + Adicionado à lista de reprodução + Autor + Doar + A Monitorizar alterações na sua biblioteca de músicas… + Lista de reprodução exportada + Doe para o projeto para ter o seu nome adicionado aqui! + Definições + Abas da biblioteca + Altere a visibilidade e a ordem das abas da biblioteca + Personalizar notificações + Lista de reprodução vazia + Importar lista de reprodução + Procurar + Filtrar + Duração + Contagem de músicas + Faixa + Data adicionada + Organizar + Ordenar por + A tocar agora + Equalizador + Misturar + Ir para o artista + Ir para o álbum + Propriedades + Visualizar + Tudo + Data + A carregar música + A carregar música + Músicas + Álbum ao vivo + Álbum remix + Solteiro + Compilação + Artista + Renomear + Eliminar + Apagar lista de reprodução? + Nova lista de reprodução + Renomear lista de reprodução + Editar + Nome + Cancelar + Adicionar + Salvar + Mudar o tema e cores da app + Tema + Claro + Automático + Caminho + Informações de erro + Copiado + Relatório + Vêr e controlar a reprodução da música + Lista de reprodução criada + Aparência + Tema preto + Utilizar tema preto puro + Modo redondo + Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) + Personalizar + Personalize os controlos e o comportamento do interface do utilizador + Escuro + Esquema de cores + Avançar para o próximo + Modo de repetição + Comportamento + Exportar lista de reprodução + Partilhar + Tamanho + Aleatório + Misturar tudo + Exportar + Sobre + Versão + Lista de reprodução renomeada + Lista de reprodução eliminada + Procurar na biblioteca… + Desligado + Configurar onde a música deve ser carregada + Pastas + Ativar ou desativar a reprodução aleatória + Seleção de imagem + Verde + Disco %d + %d Hz + Músicas carregado: %d + Álbuns carregados: %d + Artistas carregados: %d + Preferir álbum + + %d Música + %d Músicas + %d Músicas + + Demo + Normalização de volume + Ajuste de ReplayGain da faixa + Ajuste de ReplayGain do álbum + Desenvolvido por Alexander Capehart + Apoiadores + Mostrar + Personalizar a barra de reprodução + Forçar capas em formato quadrado + Recortar à capa dos álbuns numa proporção de 1:1 + Configurar o som e comportamento da reprodução + Reprodução + Retroceder antes de voltar + Retrocede a música antes de voltar para a anterior + Pausar na repetição + Pausar quando uma música é repetida + Pastas de música + Lembrar pausa + Continuar a reproduzir/pausado quando ao pular ou editar a fila + Estratégia do ganho de repetição + Prefira o álbum se estiver a tocar + +%.1f dB + -%.1f dB + %d kbps + Gêneros carregados: %d + Direção + Ascendente + Descendente + Desligado + Recarregar música + Miniálbuns + EP + EP ao vivo + Álbum de Remix + Solteiros + Single ao vivo + Single remix + Compilação ao vivo + Mistura de compilações + Trilhas sonoras + Trilha sonora + Mixtapes + Mixagem de DJ + Ao vivo + Remixes + Disco + Wiki + Ignorar ficheiros de áudio que não são música, tal como podcasts + Classificação inteligente + Ignore palavras como \"the\" ao classificar por nome (funciona melhor com músicas em inglês) + Imagens + Áudio + Pré-amplificação da normalização de volume + O pré-amplificador é aplicado ao ajuste existente durante a reprodução + Ajustar com etiquetas + Ajustar sem etiquetas + Nenhuma música encontrada + Falha ao carregar música + O Auxio precisa de permissão para ler a sua biblioteca de músicas + Música %d + Reproduzir ou pausar + Pular para a próxima música + Pular para a última música + Embaralhar todas as músicas + Ícone do Auxio + Capa do álbum + Nenhum disco + Nenhuma faixa + Nenhuma música + Nenhuma música tocando + Vermelho + Rosa + Roxo + Roxo profundo + Índigo + Azul + Verde profundo + Verde-amarelo + Amarelo + Laranja + Moreno + + %d Álbum + %d Álbuns + %d Álbuns + + + %d artista + %d artistas + %d artistas + + Ao tocar da biblioteca + Ao tocar a partir dos detalhes do item + Reproduzir do artista + Tocar a partir do gênero + Tocar música sozinha + Memorizar música misturada + Mantenha a reprodução aleatória ao reproduzir uma nova música + Ponto-e-vírgula (;) + Barra (/) + Mais (+) + Aviso: Usar essa configuração pode resultar em algumas etiquetas serem interpretadas incorretamente como tendo múltiplos valores. Pode resolver isso pré-definindo caracteres de separador indesejados com uma barra invertida (\\). + Ocultar colaboradores + Mostrar apenas artistas que foram creditados diretamente no álbum (funciona melhor em músicas com metadados completos) + Capas de álbuns + Rápido + Gênero desconhecido + Sem data + Codec de Audio Gratuito Sem Perdas (FLAC) + Ciano + Lista de reprodução %d + A carregar a sua biblioteca de músicas… (%1$d/%2$d) + Excluir %s? Não pode ser desfeito. + Taxa de bits + Taxa de amostragem + OK + Estilo de caminho + Absoluto + Relativo + Usar caminhos compatíveis com Windows + Codificação de Audio Avançada (AAC) + A editar %s + Demos + Misturas DJ + Só aparecer + Mixtape + Reproduzir a partir do item mostrado + Reproduzir de todas as músicas + Reproduzir do álbum + Conteúdo + Controlar como a música e as imagens são carregadas + Música + Recarregamento automático + Recarrega a biblioteca de músicas sempre que ela mudar (requer notificação fixa) + Excluir não-música + Separadores multi-valor + Configurar caracteres que denotam múltiplos valores de etiqueta + Vírgula (,) + E comercial (&) + Qualidade alta + Reprodução automática dos auscultadores + Iniciar música quando os auscultadores forem conectados (pode não funcionar em todos os aparelhos) + Preferir faixa + Aviso: Alterar o pré-amplificador para um valor positivo alto pode resultar em picos em algumas faixas de áudio. + Biblioteca + Recarrega a biblioteca de músicas usando metadados salvos em cache quando possível + Procurar músicas novamente + Limpa os metadados em cache e recarrega totalmente a biblioteca de música (lento, porém mais completo) + Não foi possível importar uma lista de reprodução deste ficheiro + Incapaz de exportar a lista de reprodução para este ficheiro + Nenhuma aplicação encontrada que possa executar esta tarefa + Sem pastas + Esta pasta não é compatível + Alterar o modo de repetição + Parar reprodução + Remover esta música de fila + Mover esta música da fila + Abra a fila + Mover esta guia + Limpar consulta de pesquisa + Remover pasta + Capa do álbum para %s + Imagem do artista para %s + Imagem de gênero para %s + Imagem da lista de reprodução de %s + Artista desconhecido + Sem álbuns + Áudio MPEG-1 + Áudio MPEG-4 + Áudio Ogg + Áudio Matroska + Azul profundo + Azul-verde + Grisalho + Dinâmico + %1$s, %2$s + %d Selecionadas + Duração total: %s + Mais + Iniciar a reprodução + Inicia o Auxio usando o estado salvo anteriormente. Se nenhum estado salvo estiver disponível, todas as músicas serão embaralhadas. A reprodução começará imediatamente.\n\nAVISO: Tenha cuidado ao controlar este serviço, se você fechá-lo e tentar usá-lo novamente, provavelmente irá travar o aplicativo. + Opinião + Crie um problema no GitHub + Envie um e-mail + Escolha pastas + Os seus artistas aparecerão aqui. + Álbum desconhecido + As suas músicas aparecerão aqui. + Os seus álbuns aparecerão aqui. + As suas listas de reprodução aparecerão aqui. + Os seus gêneros aparecerão aqui. + Nova pasta + Salvar espaco + MPEG-4 contendo %s + Apple Lossless Audio Codec (ALAC) + Desconhecido + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 604fc6980..851e63946 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -21,7 +21,7 @@ Adăugați la lista de așteptare A fost adăugat la lista de așteptare Mergi la artist - Accesaţi albumul + Accesați albumul Despre Versiune Cod sursă @@ -109,9 +109,6 @@ Adaugă Frecvența de eșantionare Salvează - Stat restabilit - Stat salvat - Statul a fost autorizat Gen Statisticile bibliotecii Mărime @@ -129,7 +126,7 @@ Activați colțurile rotunjite pentru elementele suplimentare ale interfeței de utilizare (necesită rotunjirea coperților albumelor) Se încarcă biblioteca muzicală… Monitorizarea bibliotecii muzicale pentru modificări… - Afişa + Afișa Utilizați o temă întunecată pur-negru Coperți rotunjite ale albumelor Listă de redare @@ -172,8 +169,8 @@ Sortează după Sortare corectă pentru nume care incep cu numere sau cuvinte precum \"the\" (funcționează cel mai bine cu melodii în limba engleză) Forțează coperți de album pătrate - Rapid - Calitate mare + Rapid + Calitate mare Punct și virgulă (;) Editează Exclude non-muzică @@ -196,7 +193,6 @@ Pornește mereu redarea când niște căști sunt conectate (s-ar putea să nu meargă pe toate dispozitivele) Re-scanează muzica Șterge memoria cache cu taguri și reîncarcă biblioteca de muzică de tot (mai încet, dar mai complet) - Restaurează starea redării Cântece încărcate %d Amestecă toate cântecele Bleu @@ -211,25 +207,18 @@ %d de artiști Arată doar artiști care sunt creditați direct pe albun (Funcționează mai bine pe bibloteci cu taguri puse bine) - Dosarul ăsta nu e suportat - Crează un nou playlist + Dosarul ăsta nu e suportat Copertă album Bibliotecă Slash (/) Deschide lista de așteptare - Salvează starea redării acum - Uită starea redării Imagine gen pentru %s Imagine playlist pentru %s Artist necunoscut - Nu s-a pututu restaura starea - Nu s-a putut salva starea Vezi mai mult Configurează caracterele care denotă mai multe valori de taguri - Foldere cu muzică - Foldere - Exclude - Muzica nu va fi încărcată din dosarele pe care le adaugi aici. + Foldere cu muzică + Foldere Fără cântece Imagine artist pentru %s Playlist importat @@ -237,7 +226,6 @@ Donează Playlist exportat Donează proiectului ca să ai numele adăugat aici! - Muzica va fi încărcată doar din folderele pe care le adaugi aici. Redă automat la conectarea căștilor N-a fost găsită nicio aplicație care poate face asta Se editează %s @@ -247,9 +235,8 @@ Redare Pauză la repetare Configurează comportamentul sunetului și redării - Salvează starea redării Fără track - Configurează de unde se încarcă muzica + Configurează de unde se încarcă muzica Reîncarcă muzica Copertă album pentru %s Gen necunoscut @@ -258,24 +245,22 @@ Mov închis Ține minte pauza Ține minte pauza atunci când dai skip printre cântece - Elimină dosarul + Elimină dosarul Imagine selecție Indigo %d Selectate Albume încărcate %d Importă playlist - Include Reîncarcă biblioteca cu muzică, folosind taguri din memoria cache Informații despre eroare Copiat Raportează Redă cântecul fără să facă parte din nicio listă Pune pauză atunci când un cântec se repetă - Mod Încărcarea muzicii a eșuat Auxio are nevoie de permisiune ca să-ți acceseze biblioteca de muzică Mută acest cântec - Niciun dosar + Niciun dosar Pornește sau oprește amestecarea Oprește redarea Elimină acest cântec diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 42fae303f..3283e2333 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -32,10 +32,9 @@ Добавлено в очередь Перейти к исполнителю Перейти к альбому - Позиция сохранена Добавить Сохранить - Нет папок + Нет папок О программе Версия Исходный код @@ -73,8 +72,6 @@ Пауза при повторе Ставить на паузу при повторе трека Библиотека - Запоминать позицию - Запоминать позицию в треке Обновить музыку Обновлять библиотеку, при возможности используя кэш тегов @@ -82,7 +79,7 @@ Ошибка чтения библиотеки Auxio требуется разрешение на чтение музыкальной библиотеки Нет приложений для открытия данной ссылки - Эта папка не поддерживается + Эта папка не поддерживается Найти в библиотеке… @@ -97,7 +94,7 @@ Переместить трек в очереди Переместить вкладку Очистить поисковый запрос - Удалить папку + Удалить папку Иконка Auxio Обложка альбома Обложка альбома %s @@ -152,7 +149,6 @@ Битрейт Диск Трек - Позиция восстановлена Отмена Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках. Сведения @@ -162,7 +158,6 @@ Частота дискретизации Предусиление применяется к существующей настройке во время воспроизведения Статистика библиотеки - Восстановить состояние воспроизведения Продолжительность Мини-альбом Мини-альбомы @@ -170,11 +165,8 @@ Дата добавления Синглы Предусиление ReplayGain - Исключить AAC - Очистить состояние воспроизведения - Музыка не будет загружена из указанных папок. - Укажите, откуда надо загружать музыку + Укажите, откуда надо загружать музыку %d кбит/с Автоматическая перезагрузка MPEG-1 @@ -189,9 +181,7 @@ Концертный альбом Концертный Мониторинг изменений в музыкальной библиотеке… - Позиция сброшена - Папки с музыкой - Включить + Папки с музыкой Альбом ремиксов Концертный мини-альбом Мини-альбом ремиксов @@ -199,14 +189,9 @@ Концертный сингл Ремикс сингл Сборники - Очистить ранее сохраненное состояние воспроизведения (если есть) Перезагружать библиотеку при изменении (требует постоянное уведомление) −%.1f дБ Жанров загружено: %d - Восстановить предыдущее состояние воспроизведения (если есть) - Режим - Музыка будет загружена только из указанных папок. - Не удалось восстановить состояние воспроизведения Нет трека %d Гц Исполнителей загружено: %d @@ -240,8 +225,8 @@ Изменение по тегам Изменения без тегов Отключены - Исходные (Быстрая загрузка) - Повышенного качества (Медленная загрузка) + Исходные (Быстрая загрузка) + Повышенного качества (Медленная загрузка) Сборник концертных записей Пользовательское поведение панели воспроизведения Пересканировать музыку @@ -253,8 +238,6 @@ %d исполнителей %d исполнителей - Не удалось очистить состояние - Не удалось сохранить состояние Предупреждение: Использование этой настройки может привести к тому, что некоторые теги будут неправильно интерпретироваться как имеющие несколько значений. Вы можете решить эту проблему, добавив к нежелательным символам-разделителям обратную косую черту (\\). %d выбрано Вики @@ -262,7 +245,7 @@ %1$s,%2$s Играть жанр Поведение - Выравнивание громкости ReplayGain + Выравнивание громкости Музыка Изображения Библиотека @@ -271,15 +254,13 @@ Управляйте загрузкой музыки и изображений Настройка звука и поведения при воспроизведении Воспроизведение - Папки - Состояние воспроизведения + Папки По убыванию Плейлист Плейлисты Обложка плейлиста для %s Игнорировать артикли при сортировке Игнорировать такие слова, как «the», при сортировке по имени (лучше всего работает с англоязычной музыкой) - Создать новый плейлист Новый плейлист Плейлист %d Добавить в плейлист @@ -337,4 +318,25 @@ Сделайте пожертвование проекту, чтобы ваше имя было добавлено сюда! Оставлять воспроизведение/паузу во время пропуска или редактирования очереди Запоминать паузу + Откл. + Запускать Auxio, используя ранее сохранённое состояние. если сохранённое состояние недоступно, все песни будут перетасованы. Воспроизведение начнётся немедленно. +\n +\nПредупреждение: будьте осторожны при управлении этой службой, если вы закроете её, а затем попытаетесь использовать снова, вы, вероятно, приведёте к сбою приложения. + Начать воспроизведение + Больше + Отзывы + Отправить электронное письмо + Выберите папки + MPEG-4, содержащий %s + Неизвестный альбом + Здесь появятся ваши исполнители. + Новая папка + Apple Lossless Audio Codec (ALAC) + Неизвестный + Здесь будут отображаться ваши плейлисты. + Ваши песни будут отображаться здесь. + Ваши альбомы будут отображаться здесь. + Здесь будут отображаться ваши жанры. + Экономия места + Сообщить о проблеме на GitHub \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 08fc56f5c..69638c1d6 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -5,7 +5,6 @@ Zbirke Albumi Zbirka remiksov - Počisti stanje predvajanja Pojdi na album Slika izvajalca za %s Smer @@ -26,7 +25,6 @@ Prednost albumu Predvajaj iz prikazanega elementa Samodejno ponovno nalaganje - Ni mogoče počistiti stanja Podaljšane Lastnosti pesmi Spremenite način ponavljanja @@ -48,15 +46,12 @@ Ko se predvaja iz podrobnosti elementa Ponovno naloži glasbo Remiksi - Shrani stanje predvajanja Opozorilo: Sprememba pred-ojačevalca na visoko pozitivno vrednost lahko privede do preseganja na nekaterih avdio posnetkih. Ni datuma Ponovno naloži glasbeno knjižnico, uporabi predpomnjene oznake, kadar je mogoče Najdena ni bila nobena aplikacija, ki bi lahko opravila to nalogo Prekliči - Vključi Seznam predvajanja %d - Shrani trenutno stanje predvajanja zdaj Preskoči na zadnjo pesem Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo) @@ -79,13 +74,11 @@ Izvajalec Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo) Zelenkasto modra - Vztrajnost Premešaj vse pesmi Seznam predavanja ustvarjen Celoten čas predvajanja: %s - Ni mogoče shraniti stanja Pavza ob ponavljanju - Mape za glasbo + Mape za glasbo Zapomni si naključno predvajanje Pojdi na izvajalca Naloženih pesmi: %d @@ -100,7 +93,6 @@ Naloženih žanrov: %d Se predvaja Odstrani to pesem - Stanje predvajanja shranjeno Ni diska Išči Vedno začnite predvajati, ko se slušalke priključijo (morda ne deluje na vseh napravah) @@ -109,13 +101,11 @@ Dodaj v čakalno vrsto Pred-ojačevalnik ReplayGain MPEG-1 Audio - Ni mogoče obnoviti stanja Spremenite temo in barve aplikacije Poskusi znova Prilagodi zvok in obnašanje predvajanja Nadzorujte kako se glasba in slike nalagajo Izgled in občutek - Izključi Matroska Audio Začasna prekinitev ob ponavljanju Predvajaj @@ -126,15 +116,14 @@ Pred-ojačevalec se uporablja na obstoječi prilagoditvi med predvajanjem Predvajaj iz albuma Glasba - Ta mapa ni podprta - Obnovi prej shranjeno stanje predvajanja (če obstaja) + Ta mapa ni podprta Razvil Alexander Capehart - Odstrani mapo + Odstrani mapo Kopirano Nalaganje glasbe ni uspelo Album Ko se predvaja iz knjižnice - Visoka kvaliteta + Visoka kvaliteta Prilagoditev brez oznak Dodaj na seznam predvajanja Datum vnosa @@ -144,7 +133,6 @@ Naslovnica albuma Preimenuj Plus (+) - Stanje predvajanja obnovljeno %d Izbrano Neznan izvajatelj Slike @@ -154,7 +142,6 @@ %d izvajalci %d izvajalcev - Stanje predvajanja počiščeno Prikaz %1$s, %2$s Ogg Audio @@ -165,7 +152,7 @@ Ni pesmi Podaljšano Pesmi - Mape + Mape Prireži vse naslovnice albumov v razmerje 1:1 Prilagojeno dejanje na vrstici za predvajanje Indigo modra @@ -200,13 +187,10 @@ Naslovnica albuma za %s V živo Spremenite vidnost in vrstni red zavihkov knjižnice - Počisti shranjeno stanje predvajanja (če obstaja) Sortiraj po Ogled Ustavi predvajanje Mežanica - Glasba se bo nalagala samo iz map, ki jih dodate. - Način Remiks singla Auxio potrebuje dovoljenje za branje vaše glasbene knjižnice Tema @@ -216,7 +200,7 @@ Premakni ta zavihek Pesem Slika žanra za %s - Nastavitev virov za nalaganje glasbe + Nastavitev virov za nalaganje glasbe DJ Miksi Odstrani seznam predvajanja\? Modra @@ -224,7 +208,6 @@ Zaobljen način Naloženih izvajalcev: %d Zvok - Glasba se ne bo nalagala iz the map, ki jih dodate. Rdeča Dinamično Temno @@ -232,12 +215,10 @@ Žanr Predvajanje Vredu - Ustvari nov seznam predvajanja Singl Seznam predvajanja odstranjen Dovoli Predvajaj iz vseh pesmi - Obnovi stanje predvajanja Prilagoditev z oznakami Predvajanje ob priključitvi slušalk Vejica (,) @@ -269,19 +250,19 @@ Padajoče Podaljšan remiks Podpičje (;) - Hitro + Hitro Seznam predvajanja preimenovan Predvajaj ali začasno ustavi Rjava ReplayGain strategija Izvorna koda Predvajaj iz izvajalca - Ni map + Ni map Prilagoditev kontrol uporabniškega vmesnika in obnašanja Hitrost vzorčenja Čakalna vrsta Ločila za več vrednosti - ReplayGain Tehnologija + Normalizacija glasnosti Singli Pregled in nadzor predvajanja glasbe Uporabite čisto črno temo @@ -302,4 +283,31 @@ Limeta Sodeloval pri %d Hz + Izključeno + Ni mogoče izvoziti seznama predvajanja v to datoteko + Uvožen seznam predvajanja + Demo posnetki + Demo + ReplayGain Prilagoditev Posnetka + ReplayGain Prilagoditev Albuma + Avtor + Donacija + Podporniki + Seznam predvajanja uspešno uvožen + Seznam predvajanja uspešno izvožen + Donirajte projektu, da se vaše ime doda tukaj! + Ni albumov + Zapomni si pavzo + Predvajaj/pavza med preskakovanjem ali urejanjem čakalne vrste + Prazen seznam predvajanja + Uvozite seznam predvajanja + Ni mogoče uvoziti seznama predvajanja iz te datoteke + Uvozite + Pot + Uporabi poti kompatibilne z Windows + Izvozite seznam predvajanja + Stil poti + Izvozite + Absolutna + Relativna \ No newline at end of file diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml new file mode 100644 index 000000000..de17dd795 --- /dev/null +++ b/app/src/main/res/values-sq/strings.xml @@ -0,0 +1,325 @@ + + + Ngarkimi i muzikës + Po ngarkohet muzika + Një luajtës muzikor i thjeshtë dhe racional për android. + Kompilim Live + Monitorimi i bibliotekës muzikore + Përsëritje + Më shumë + Këngët + Këngë + Të gjitha këngët + Albumet + Albumi + Album live + Album remix + EPs + EP + EP Live + EP Remix + Këngët + Këng + Këng Live + Këng Remix + Kompilimet + Kompilime + Kompilim Remix + Soundtracks + Soundtrack + Mixtapes + Mixtape + Riemërto listën e këngëve + Lejoj + Demo + Demos + DJ Mixes + DJ Mix + Jetoni + Remixes + Shfaqet në + Artist + Artistët + Zhanri + Zhanret + Lista e dëgjimit + Listat e dëgjimit + List të rej + Lista e dëgjimit bosh + Lista e importuar + Importo + Importo listën e këngëve + Eksporto + Eksporto listën e këngëve + Riemërto + Fshije + Të fshihet lista e këngëve? + Modifiko + Kërko + Filtro + Të gjitha + Emri + Data + Kohëzgjatja + Tani po luan + Numri i këngëve + Disku + Pista + Data e shtimit + Rendit + Rendit sipas + Drejtimi + Në ngjitje + Në zbritje + Përziej + Radhë + Ekualizues + Luaj + Luaj tjetrën + Shto në radhë + Shto në listën e luajtjes + Shko te albumi + Shko te artisti + Shiko vetitë + Shiko + Shpërndaje + Cilësitë e këngës + Formati + Madhësia + Shpejtësia e bitit + Përziej + Përziej të gjitha + Fillo riprodhimin + OK + Anulo + Ruaj + Rivendos + Shto + Më shumë + Absolute + Relativ + Rreth + Versioni + Kodi burimor + Wiki + Licencat + Statistikat e bibliotekës + Përzgjedhja + Informacioni i gabimit + E kopjuar + Raportoni + Autori + Alexander Capehart + Dhuroni + Mbështetësit + Shikoni dhe kontrolloni riprodhimin e muzikës + Biblioteka juaj muzikore po ngarkohet… + Duke monitoruar bibliotekën tuaj të muzikës për ndryshime… + Lista e këngëve u krijua + Lista e këngëve u importua + Lista e muzikës e rinovuar + Lista e këngëve e eksportuar + Lista e muzikës u fshi + U shtuar në listën e këngëve + Dhuroni në projekt për të shtuar emrin tuaj këtu! + Kërkoni në bibliotekën tuaj… + Shtuar në radhë + Norma e mostrës + Rregullimi i gjurmës ReplayGain + Rregullimi i albumit ReplayGain + Rruga + Stili i rrugës + Përdorni shtigje të pajtueshme me Windows + Ndryshoni temën dhe ngjyrat e aplikacionit + Tema + Automatik + Nis Auxio duke përdorur gjendjen e ruajtur më parë. Nëse nuk ka një gjendje të ruajtur, të gjitha këngët do të luhen në mënyrë të rastësishme. Riprodhimi do të fillojë menjëherë. \n\nKUJDES: Kini kujdes kur kontrolloni këtë shërbim; nëse e mbyllni dhe pastaj përpiqeni ta përdorni përsëri, ka mundësi që aplikacioni të bllokohet. + Cilësimet + Shikoni dhe ndjeni + E ndritshme + E errët + Skema e ngjyrave + Tema e zezë + Përdorni një temë të errët në të zezë + Veprim i personalizuar i shiritit të riprodhimit + Veprim i personalizuar i njoftimit + Kalo te tjetra + Mënyra e përsëritjes + Sjellja + Kur luani nga biblioteka + Kur luani nga detajet e artikullit + Luaj nga artikulli i treguar + Luaj nga të gjitha këngët + Luaj nga albumi + Luaj nga artisti + Luaj nga zhanri + Luaj këngën vetë + prerje (/) + Plus (+) + Modaliteti i rrumbullakët + Personalizoje + shfaqja + Skedat e bibliotekës + Mbani mend përzierjen + Vazhdoni të aktivizoni përzierjen kur luani një këngë të re + Kontrolloni se si ngarkohen muzika dhe imazhet + Muzikë + Përjashto jo-muzikën + Injoroni skedarët audio që nuk janë muzikë, të tilla si podkastet + Ndarës me shumë vlera + Ampersand (&) + Kujdes: Përdorimi i këtij cilësimi mund të rezultojë në interpretimin e gabuar të disa etiketave si të kenë vlera të shumta. Ju mund ta zgjidhni këtë duke e paraqitur karakterin ndarës të padëshiruar me një backslash (\\). + Renditja inteligjente + Fshih bashkëpunëtorët + Imazhet + Fikur + Shpejt + Forco kopertinat e albumeve katrore + Pritini të gjitha kopertinat e albumeve në një raport pamjeje 1:1 + Audio + Konfiguro sjelljen e zërit dhe riprodhimit + Riprodhimi + Luajtja automatike me kufje + Gjithmonë filloni të luani kur një kufje është e lidhur (mund të mos funksionojë në të gjitha pajisjet) + Ktheje prapa përpara se të kapërcesh prapa + Kthejeni prapa përpara se të kaloni te kënga e mëparshme + Pushoni në përsëritje + Pushoni kur një këngë përsëritet + Krijo një çështje në GitHub + Dërgo një email + Përshtypje + Kujto pushimin + Aktivizo qoshet e rrumbullakosura në elementë shtesë të ndërfaqes së përdoruesit (kërkon që kopertinat e albumit të jenë të rrumbullakosura) + Ndryshoni dukshmërinë dhe renditjen e skedave të bibliotekës + Personalizo kontrollet dhe sjelljen e UI + përmbajtja + Rimbushje automatike + Rifresko bibliotekën muzikore sa herë që ndryshon (kërkon njoftim të vazhdueshëm) + Konfiguro karakteret që tregojnë vlera të shumta etiketash + pikëpresje (;) + Presja (,) + Renditni saktë emrat që fillojnë me numra ose fjalë si \"the\" (funksionon më së miri me muzikën në anglisht). + Kopertinat e albumeve + Shfaq vetëm artistë që vlerësohen drejtpërdrejt në një album (funksionon më mirë në bibliotekat e etiketuara mirë) + Cilësi e lartë + Normalizimi i volumit + Mbajeni duke luajtur/ndërprerë kur kaloni ose redaktoni radhën + Strategjia ReplayGain + Fike + Prefero këngën + +%.1f dB + %d Hz + + %d album + %d albume + + Dosjet e muzikës + Kjo dosje nuk mbështetet + Ndryshoni mënyrën e përsëritjes + Hiq këtë këngë + Free Lossless Audio Codec (FLAC) + Gri + Dinamik + %1$s, %2$s + %d e zgjedhur + Duke redaktuar %s + Disc %d + Lista e këngëve %d + + %d këngë + %d këngë + + + %d artist + %d artistë + + Lëviz këtë skedë + Imazhi i listës së këngëve për %s + Nuk ka këngë + Prefero albumin + Prefero albumin nëse njëri është duke luajtur + ReplayGain pre-amp + Përforcuesi paraprak aplikohet në rregullimin ekzistues gjatë riprodhimit. + Rregullim me etiketa + Rregullim pa etiketa + Kujdes: Ndryshimi i pre-amplitudës në një vlerë të lartë pozitive mund të shkaktojë teprim në disa shtegtarë audio. + Biblioteka + Menaxho se nga ku duhet të ngarkohet muzika + Dosjet + Rifresko muzikën + Rifreskoni bibliotekën muzikore, duke përdorur etiketat e ruajtura kur është e mundur + Rikërkoni muzikën + Pastroni cache-n e etiketave dhe rifreskoni plotësisht bibliotekën muzikore (më ngadalë, por më të plotë) + Nuk u gjet muzikë + Ngarkimi i muzikës dështoi + Auxio ka nevojë për leje për të lexuar bibliotekën tuaj muzikore + Nuk mund të importoni një listë këngësh nga ky skedar + Nuk mund të eksportoni listën e këngëve në këtë skedar + Nuk u gjet asnjë aplikacion që mund ta përballojë këtë detyrë + Nuk ka dosje + Kënga %d + Luaj ose ndalo + Kaloni te kënga tjetër + Kaloni te kënga e fundit + Aktivizoni ose çaktivizoni përzierjen + Përziej të gjitha këngët + Ndalo riprodhimin + Lëviz këtë këngë + Hap radhën + Pastro kërkimin + Hiq dosjen + Ikona e Auxio + Kopertina e albumit + Kopertina e albumit për %s + Imazhi i artistit për %s + Imazhi i zhanrit për %s + Imazhi i përzgjedhjes + Artist i panjohur + Zhanër i panjohur + Nuk ka datë + Nuk ka disqe + Nuk ka këngë + Nuk ka albume + Nuk po luhet muzikë + MPEG-1 audio + MPEG-4 audio + Ogg audio + Matroska audio + Advanced Audio Coding (AAC) + Kuq + Rozë + Vjollcë + Vjollcë e thellë + Blu + Blu e thellë + Cian + Teal + Gjelbërt + Gjelbër e thellë + Gëlqere + Verdh + Portokall + Kafe + -%.1f dB + %d kbps + Duke ngarkuar bibliotekën tuaj muzikore... (%1$d/%2$d) + Këngët e ngarkuara: %d + Albumet e ngarkuara: %d + Artistët e ngarkuar: %d + Zhanret e ngarkuara: %d + Kohëzgjatja totale: %s + A dëshironi të fshini %s? Kjo nuk mund të anulohet. + Lejla + Zgjidh dosjet + Album i panjohur + Këngët e tua do të shfaqen këtu. + Albumet tuaja do të shfaqen këtu. + Artistët e tu do të shfaqen këtu. + Listat tuaja për luajtje do të shfaqen këtu. + Zhanret tuaja do të shfaqen këtu. + Dosje e re + Apple Lossless Audio Codec (ALAC) + E panjohur + Kurseni hapësirë + MPEG-4 që përmban %s + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8043ac526..f0c5a6b39 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -40,13 +40,13 @@ Datum tillagt Stigande Fallande - Nu spelar + Spelas nu Utjämnare Spela Blanda Spela nästa - Lägg till spellista + Lägg till i spellista Gå till artist Gå till album Visa egenskaper @@ -60,7 +60,6 @@ Okej Avbryt Spara - Tillstånd återställt Om Källkod Wiki @@ -70,12 +69,12 @@ Övervakar ändringar i ditt musikbibliotek… Tillagd i kö Spellista skapad - Tillagd till spellista + Tillagd i spellista Sök i ditt musikbibliotek… Inställningar Utseende Ändra färger och tema - Automatisk + Automatiskt Ljust Svart tema Runt läge @@ -97,16 +96,14 @@ Alla Disk Sortera - Lägg till kö + Lägg till i kö Lägg till - Tillstånd togs bort Överföringskapacitet Återställ - Tillstånd sparat Version Bibliotekstatistik Bytt namn på spellista - Spellista tog bort + Spellistan togs bort Alexander Capeheart Tema Mörkt @@ -147,12 +144,11 @@ Komma (,) Snedstreck (/) Konfigurera tecken som separerar flera värden i taggar - Varning: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\). + Varning: Denna inställning kan leda till att vissa etiketter separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett omvänt snedstreck (). Anpassa UI-kontroller och beteende Av Autouppspelning med hörlurar Pausa när en låt upprepas - Musik laddas inte från mapparna som ni lägger till. Öppna kön Dynamisk Inlästa artister: %d @@ -165,26 +161,19 @@ Konfigurera ljud- och uppspelningsbeteende Spola tillbaka innan spår hoppar tillbaka ReplayGain-strategi - Rensa det tidigare sparade uppspelningsläget om det finns - Återställ uppspelningsläge -%.1f dB Ta bort %s? Detta kan inte ångras. Endast visa artister som är direkt krediterade på ett album (funkar bäst på välmärkta bibliotek) Albumomslag - Snabbt + Snabbt Bibliotek - Inkludera Uppdatera musik - Läs in musik på nytt, vid möjlighet med användning av cachade taggar - Persistens - Rensa uppspelningsläge - Återställ det tidigare lagrade uppspelningsläget om det finns - Misslyckades att spara uppspelningsläget + Läs in musik på nytt, vid möjlighet med användning av cachade etiketter Blanda alla låtar Rensa sökfrågan - Ta bort mapp + Ta bort mapp Genrebild för %s - Bild spellista för %s + Spellistabild för %s MPEG-1-ljud MPEG-4-ljud OGG-ljud @@ -203,26 +192,21 @@ Uppspelning Orange Brun - Alltid börja uppspelning när hörlurar kopplas till (kanske inte fungerar på alla enheter) + Påbörja alltid uppspelning när hörlurar kopplas till (kanske inte fungerar på alla enheter) Pausa vid upprepning ReplayGain försteg - Justering utan taggar - Musikmappar - Varning: Om man ändrar förförstärkaren till ett högt positivt värde kan det leda till toppning på vissa ljudspår. - Hantera vart musik läses in ifrån - Mappar - Modus - Utesluta - Musik laddas endast från mapparna som ni lägger till. - Spara aktuellt uppspelningsläge + Justering utan etiketter + Musikmappar + Varning: Att ändra förförstärkaren till ett högt positivt värde kan leda till ett förhöjt ljudtryck på vissa ljudspår. + Hantera var musik läses in ifrån + Mappar Skanna om musik - Rensa tagbiblioteket och ladda komplett om musikbiblioteket (långsammare, men mer komplett) - Ingen musik på gång - Läsa in musik misslyckades + Rensa etikettbiblioteket och ladda komplett om musikbiblioteket (långsammare, men mer komplett) + Ingen musik hittades + Musikinläsning misslyckades Auxio måste ges behörighet för att läsa in ditt musikbibliotek Ingen lämplig app kunde hittas - Denna mapp stöds inte - Misslyckades att återställa uppspelningsläget + Denna mapp stöds inte Spår %d Spela eller pausa Flytta detta spår @@ -256,14 +240,12 @@ Inlästa album: %d Inlästa genrer: %d Spela endast vald låt - Hög kvalitet + Hög kvalitet Tvinga fyrkantiga skivomslag Beskär alla albumomslag till ett 1:1 sidförhållande Spola tillbaka innan att hoppa till föregående låt - Justering med taggar - Inga mappar - Misslyckades att rensa uppspelningsläget - Skapa en ny spellista + Justering med etiketter + Inga mappar Stoppa uppspelning Ta bort låt Auxio-ikon @@ -273,7 +255,6 @@ Mörklila Indigo Skiva %d - Spara uppspelningsläge Hoppa till nästa låt Hoppa till sista låt Ändra upprepningsläge @@ -285,7 +266,7 @@ Rosa Laddar ditt musikbibliotek… (%1$d/%2$d) Ampersand (&) - ReplayGain + Volymnormalisering Föredra spår Föredra album Föredra album om ett album spelar @@ -299,16 +280,16 @@ Riktning Demo Demos - Upphovsperson + Utvecklare Spellistan har importerats Spellistan har exporterats - Skänk projektet ett bidrag så lägger vi till ditt namn här! + Bidra till projektet så läggs ditt namn till här! Kom ihåg pausat läge Fortsätt uppspelning/pausat läge vid spårbyte och listredigering Kunde inte importera spellistan till denna fil Tom spellista Importera spellista - Vy + Visa Sökväg ReplayGain Spårbaserad Volymjustering ReplayGain Albumbaserad Volymjustering @@ -322,4 +303,23 @@ Relativ Använd Windowskompatibla sökvägar Inga album - \ No newline at end of file + Av + Välj mappar + Skapa ett ärende på GitHub + Mer + Okänt album + Ny mapp + Okänt + Skicka ett mejl + Feedback + Dina album kommer visas här. + Dina låtar kommer visas här. + Dina artister kommer visas här. + Dina spellistor kommer visas här. + Dina genrer kommer visas här. + Apples förlustfria ljudkodek (ALAC) + Påbörja uppspelning + Spara utrymme + MPEG-4 innehållande %s + Påbörjar Auxio med det tidigare sparade tillståndet. Om inget sparad tillstånd är tillgängligt kommer alla låtar blandas. Uppspelning börjar omedelbart. \n\nOBS: Var försiktig vid användning av denna tjänst, om du stänger ner den och sedan försöker använda den igen kommer du troligen få appen att krascha. + diff --git a/app/src/main/res/values-sw600dp/styles_ui.xml b/app/src/main/res/values-sw600dp/styles_ui.xml index 3a3175896..1d55cdcbb 100644 --- a/app/src/main/res/values-sw600dp/styles_ui.xml +++ b/app/src/main/res/values-sw600dp/styles_ui.xml @@ -1,10 +1,5 @@ - - - diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml new file mode 100644 index 000000000..bb6179527 --- /dev/null +++ b/app/src/main/res/values-ta/strings.xml @@ -0,0 +1,314 @@ + + + மிக்ச்டேப் + டி.சே கலவைகள் + வாழ + ஏற்றுமதி பிளேலிச்ட் + காலம் + பாடல் எண்ணிக்கை + வட்டு + மின்தடம் + மீட்டமை + கூட்டு + அமைப்புகள் + பாருங்கள் மற்றும் உணருங்கள் + இருண்ட + இடைமுகம் கட்டுப்பாடுகள் மற்றும் நடத்தை தனிப்பயனாக்கவும் + தனிப்பயன் அறிவிப்பு நடவடிக்கை + அடுத்து செல்லவும் + மீண்டும் பயன்முறை + பல மதிப்பு பிரிப்பான்கள் + பிளச் (+) + ஆம்பர்சண்ட் (&) + ஒத்துழைப்பாளர்களை மறைக்கவும் + படங்கள் + ஆடியோ + குறிச்சொல் தற்காலிக சேமிப்பை அழித்து, இசை நூலகத்தை முழுமையாக மீண்டும் ஏற்றவும் (மெதுவாக, ஆனால் இன்னும் முழுமையானது) + தேதி இல்லை + வட்டு இல்லை + தடமில்லை + பாடல்கள் இல்லை + ஆரஞ்சு + பழுப்பு + சாம்பல் + மாறும் + %1$s, %2$s + %d தேர்ந்தெடுக்கப்பட்டது + திருத்துதல் %s + வட்டு %d + பிளேலிச்ட் %d + +%.1f டெசிபெல் + பாடல்கள் ஏற்றப்பட்டன: %d + + %d ஆல்பம் + %d ஆல்பங்கள் + + + %d கலைஞர் + %d கலைஞர்கள் + + ஆண்ட்ராய்டு க்கான எளிய, பகுத்தறிவு இசை வீரர். + இசை ஏற்றுதல் + இசையை ஏற்றுகிறது + கண்காணிப்பு இசை நூலகம் + மீண்டும் முயற்சிக்கவும் + கோப்புறைகளைத் தேர்ந்தெடுங்கள் + மேலும் + மானியம் + பாடல்கள் + பாடல் + அனைத்து பாடல்களும் + ஆல்பம் + ஆல்பம் + நேரடி ஆல்பம் + ரீமிக்ச் ஆல்பம் + இபிஎச் + ஈ.பி. + ஒற்றையர் + ஒற்றை + ஒற்றை வாழ + ரீமிக்ச் ஒற்றை + தொகுப்புகள் + தொகுப்பு + ரீமிக்ச் ஈபி + நேரடி தொகுப்பு + ரீமிக்ச் தொகுப்பு + ஒலிப்பதிவுகள் + ஒலிப்பதிவு + மிக்ச்டேப்ச் + டெமோ + டெமோச் + டி.சே கலவை + ரீமிக்ச் + தோன்றும் + கலைஞர் + கலைஞர்கள் + வகை + வகைகள் + பிளேலிச்ட் + பிளேலிச்ட்கள் + புதிய பிளேலிச்ட் + வெற்று பிளேலிச்ட் + இறக்குமதி செய்யப்பட்ட பிளேலிச்ட் + இறக்குமதி + பிளேலிச்ட்டை இறக்குமதி செய்யுங்கள் + ஏற்றுமதி + மறுபெயரிடுங்கள் + பிளேலிச்ட்டை மறுபெயரிடுங்கள் + நீக்கு + பிளேலிச்ட்டை நீக்கவா? + தொகு + தேடல் + வடிப்பி + அனைத்தும் + பெயர் + திகதி + தேதி சேர்க்கப்பட்டது + வரிசைப்படுத்து + வரிசைப்படுத்தவும் + திசை + ஏறுதல் + இறங்கு + இப்போது விளையாடுகிறது + சமநிலைப்படுத்தி + விளையாடுங்கள் + கலக்கு + வரிசை + அடுத்து விளையாடுங்கள் + வரிசையில் சேர்க்கவும் + பிளேலிச்ட்டில் சேர்க்கவும் + கலைஞரிடம் செல்லுங்கள் + ஆல்பத்திற்குச் செல்லுங்கள் + பண்புகளைக் காண்க + பார்வை + பங்கு + பாடல் பண்புகள் + பாதை + வடிவம் + அளவு + துகள் வீதம் + மாதிரி வீதம் + Replaygain மின்தடம் சரிசெய்தல் + மறுபதிப்பு ஆல்பம் சரிசெய்தல் + கலக்கு + அனைத்தையும் மாற்றவும் + பிளேபேக்கைத் தொடங்குங்கள் + சரி + ரத்துசெய் + சேமி + மேலும் + பாதை நடை + தனி, சார்பிலா + உறவினர் + விண்டோச்-இணக்கமான பாதைகளைப் பயன்படுத்தவும் + பற்றி + பதிப்பு + மூலக் குறியீடு + விக்கி + உரிமங்கள் + நூலக புள்ளிவிவரங்கள் + தேர்வு + பிழை செய்தி + நகலெடுக்கப்பட்டது + அறிக்கை + நூலாசிரியர் + அலெக்சாண்டர் கேப்ஆர்ட் + கருத்து + கிதுபில் ஒரு சிக்கலை உருவாக்குங்கள் + மின்னஞ்சல் அனுப்புங்கள் + நன்கொடை + ஆதரவாளர்கள் + இசை பின்னணியைக் காணவும் கட்டுப்படுத்தவும் + உங்கள் இசை நூலகத்தை ஏற்றுகிறது… + மாற்றங்களுக்காக உங்கள் இசை நூலகத்தை கண்காணித்தல்… + வரிசையில் சேர்க்கப்பட்டது + பிளேலிச்ட் உருவாக்கப்பட்டது + பிளேலிச்ட் இறக்குமதி செய்யப்பட்டது + பிளேலிச்ட் மறுபெயரிடப்பட்டது + பிளேலிச்ட் ஏற்றுமதி செய்யப்பட்டது + பிளேலிச்ட் நீக்கப்பட்டது + பிளேலிச்ட்டில் சேர்க்கப்பட்டது + உங்கள் பெயரை இங்கே சேர்க்க திட்டத்திற்கு நன்கொடை அளிக்கவும்! + உங்கள் நூலகத்தைத் தேடுங்கள்… + முன்னர் சேமிக்கப்பட்ட நிலையைப் பயன்படுத்தி ஆக்சியோவைத் தொடங்குகிறது. சேமிக்கப்பட்ட நிலை எதுவும் கிடைக்கவில்லை என்றால், எல்லா பாடல்களும் மாற்றப்படும். பிளேபேக் உடனடியாக தொடங்கும்.\n\n எச்சரிக்கை: இந்த சேவையை கட்டுப்படுத்துவதில் கவனமாக இருங்கள், நீங்கள் அதை மூடிவிட்டு அதை மீண்டும் பயன்படுத்த முயற்சித்தால், நீங்கள் பயன்பாட்டை செயலிழக்கச் செய்வீர்கள். + பயன்பாட்டின் கருப்பொருள் மற்றும் வண்ணங்களை மாற்றவும் + கருப்பொருள் + தானியங்கி + ஒளி + வண்ணத் திட்டம் + கருப்பு கருப்பொருள் + தூய-கருப்பு இருண்ட கருப்பொருளைப் பயன்படுத்தவும் + சுற்று பயன்முறை + கூடுதல் இடைமுகம் கூறுகளில் வட்டமான மூலைகளை இயக்கவும் (ஆல்பம் கவர்கள் வட்டமிட வேண்டும்) + தனிப்பயனாக்கு + காட்சி + நூலக தாவல்கள் + நூலக தாவல்களின் தெரிவுநிலை மற்றும் வரிசையை மாற்றவும் + தனிப்பயன் பின்னணி பார் நடவடிக்கை + நடத்தை + நூலகத்திலிருந்து விளையாடும்போது + உருப்படி விவரங்களிலிருந்து விளையாடும்போது + காட்டப்பட்ட உருப்படியிலிருந்து விளையாடுங்கள் + எல்லா பாடல்களிலிருந்தும் விளையாடுங்கள் + ஆல்பத்திலிருந்து விளையாடுங்கள் + கலைஞரிடமிருந்து விளையாடுங்கள் + வகையிலிருந்து விளையாடுங்கள் + பாடலை தானே வாசிக்கவும் + கலக்கு நினைவில் கொள்ளுங்கள் + புதிய பாடலை இசைக்கும்போது கலக்கிக் கொள்ளுங்கள் + உள்ளடக்கம் + இசை மற்றும் படங்கள் எவ்வாறு ஏற்றப்படுகின்றன என்பதைக் கட்டுப்படுத்தவும் + இசை + தானியங்கி மறுஏற்றம் + இசை நூலகத்தை மாற்றும்போதெல்லாம் மீண்டும் ஏற்றவும் (தொடர்ச்சியான அறிவிப்பு தேவை) + இசை அல்லாதவை + பாட்காச்ட்கள் போன்ற இசை இல்லாத ஆடியோ கோப்புகளை புறக்கணிக்கவும் + பல குறிச்சொல் மதிப்புகளைக் குறிக்கும் எழுத்துக்களை உள்ளமைக்கவும் + கமா (,) + அரைப்புள்ளி (;) + (பழம்) குறைத்தல் + எச்சரிக்கை: இந்த அமைப்பைப் பயன்படுத்துவதால் சில குறிச்சொற்கள் பல மதிப்புகளைக் கொண்டிருப்பதாக தவறாக விளக்கலாம். பின்சாய்வுக்கோடான (\\) மூலம் தேவையற்ற பிரிப்பான் எழுத்துக்களை முன்னொட்டு செய்வதன் மூலம் இதை நீங்கள் தீர்க்கலாம். + நுண்ணறிவு வரிசையாக்கம் + எண்கள் அல்லது \"தி\" போன்ற சொற்களுடன் தொடங்கும் பெயர்களை சரியாக வரிசைப்படுத்தவும் (ஆங்கில மொழி இசையுடன் சிறப்பாக செயல்படுகிறது) + ஒரு ஆல்பத்தில் நேரடியாக வரவு வைக்கப்படும் கலைஞர்களை மட்டுமே காண்பி (நன்கு குறியிடப்பட்ட நூலகங்களில் சிறப்பாக செயல்படுகிறது) + ஆல்பம் கவர்கள் + அணை + வேகமாக + உயர் தகுதி + படை சதுர ஆல்பம் கவர்கள் + அனைத்து ஆல்பத்தையும் 1: 1 விகித விகிதத்திற்கு பயிர் + ஒலி மற்றும் பின்னணி நடத்தை உள்ளமைக்கவும் + பின்னணி + எட்செட் ஆட்டோபிளே + எட்செட் இணைக்கப்படும்போது எப்போதும் விளையாடத் தொடங்குங்கள் (எல்லா சாதனங்களிலும் வேலை செய்யாது) + பின்வாங்குவதற்கு முன் முன்னாடி + முந்தைய பாடலைத் தவிர்ப்பதற்கு முன் முன்னாடி + ஒரு பாடல் மீண்டும் நிகழும்போது இடைநிறுத்துங்கள் + இடைநிறுத்தம் நினைவில் கொள்ளுங்கள் + வரிசையைத் தவிர்க்கும்போது அல்லது திருத்தும்போது விளையாட/இடைநிறுத்தப்படுங்கள் + தொகுதி இயல்பாக்கம் + Replaygain மூலோபாயம் + அணை + பாதையை விரும்புகிறேன் + ஆல்பத்தை விரும்புங்கள் + ஒருவர் விளையாடினால் ஆல்பத்தை விரும்புங்கள் + Replaygain pre-amp + பிளேபேக்கின் போது இருக்கும் சரிசெய்தலுக்கு முன் ஆம்ப் பயன்படுத்தப்படுகிறது + குறிச்சொற்களுடன் சரிசெய்தல் + குறிச்சொற்கள் இல்லாமல் சரிசெய்தல் + எச்சரிக்கை: முன் ஆம்பை அதிக நேர்மறையான மதிப்புக்கு மாற்றுவது சில ஆடியோ தடங்களில் உயர்ந்தது. + நூலகம் + இசை கோப்புறைகள் + இசையை எங்கிருந்து ஏற்ற வேண்டும் என்பதை நிர்வகிக்கவும் + கோப்புறைகள் + இசையைப் புதுப்பிக்கவும் + மீண்டும்இயக்கையில் இடைநிறுத்தம் + முடிந்தவரை தற்காலிக சேமிப்பு குறிச்சொற்களைப் பயன்படுத்தி இசை நூலகத்தை மீண்டும் ஏற்றவும் + ரெசான் மியூசிக் + இசை எதுவும் கிடைக்கவில்லை + இசை ஏற்றுதல் தோல்வியடைந்தது + உங்கள் இசை நூலகத்தைப் படிக்க ஆக்சியோவுக்கு இசைவு தேவை + இந்த கோப்பிலிருந்து ஒரு பிளேலிச்ட்டை இறக்குமதி செய்ய முடியவில்லை + இந்த கோப்பில் பிளேலிச்ட்டை ஏற்றுமதி செய்ய முடியவில்லை + இந்த பணியைக் கையாளக்கூடிய எந்த பயன்பாடும் கிடைக்கவில்லை + கோப்புறைகள் இல்லை + இந்த கோப்புறை ஆதரிக்கப்படவில்லை + ட்ராக் %d + விளையாடுங்கள் அல்லது இடைநிறுத்தம் + அடுத்த பாடலுக்குச் செல்லுங்கள் + கடைசி பாடலுக்குச் செல்லுங்கள் + மீண்டும் பயன்முறையை மாற்றவும் + கலக்கலை ஆன் அல்லது ஆஃப் செய்யுங்கள் + எல்லா பாடல்களையும் மாற்றவும் + பிளேபேக்கை நிறுத்துங்கள் + இந்த பாடலை அகற்று + இந்த பாடலை நகர்த்தவும் + வரிசையைத் திறக்கவும் + இந்த தாவலை நகர்த்தவும் + தேடல் வினவலை அழிக்கவும் + கோப்புறையை அகற்று + ஆக்சியோ படவுரு + ஆல்பம் கவர் + %s ஆல்பம் கவர் + %s க்கான கலைஞர் படம் + %s க்கான வகை படம் + %s க்கான பிளேலிச்ட் படம் + தேர்வு படம் + தெரியாத கலைஞர் + தெரியாத வகை + ஆல்பம் + இசை இல்லை + MPEG-1 ஆடியோ + MPEG-4 ஆடியோ + OGG ஆடியோ + திருமண ஆடியோ + மேம்பட்ட ஆடியோ குறியீட்டு முறை (AAC) + இலவச இழப்பு இல்லாத ஆடியோ கோடெக் (FLAC) + சிவப்பு + இளஞ்சிவப்பு + ஊதா + ஆழமான ஊதா + இண்டிகோ + நீலம் + ஆழமான நீலம் + சியான் + டீல் + பச்சை + ஆழமான பச்சை + சுண்ணாம்பு + மஞ்சள் + %d kbps + %d hz + உங்கள் இசை நூலகத்தை ஏற்றுகிறது… (%1$d/%2$d) + %s ஐ நீக்கவா? இதை செயல்தவிர்க்க முடியாது. + ஆல்பங்கள் ஏற்றப்பட்டன: %d + கலைஞர்கள் ஏற்றப்பட்டனர்: %d + வகைகள் ஏற்றப்பட்டவை: %d + மொத்த காலம்: %s + + %d பாடல் + %d பாடல்கள் + + நிகழ்நிலை ஈபி + -%.1f டெசிபெல் + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 16eacfc8a..d04d0cc05 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -77,7 +77,6 @@ Albüm Yıl Süre - Durum kaydedildi Alexander Capehart tarafından geliştirildi Siyah tema Kitaplık istatistikleri @@ -93,8 +92,7 @@ Gösterilen öğeden çal Tüm şarkılardan çal Albümden çal - Müzik klasörleri - Müzik yalnızca eklediğiniz klasörlerden yüklenecektir. + Müzik klasörleri %s Albümünün kapağı %s Sanatçısının resmi Parça yok @@ -145,14 +143,12 @@ Önceki şarkıya atlamadan önce geri sar Bir şarkı tekrarlandığında duraklat İçerik - Çalma durumunu kaydet - Mevcut çalma durumunu şimdi kaydet Karıştırmayı hatırla Yeni bir şarkı çalarken karışık çalmayı açık tut Müziği yenile Mümkün olduğunda önbelleğe alınmış etiketleri kullanarak müzik kitaplığını yeniden yükleyin - Klasör yok - Bu klasör desteklenmiyor + Klasör yok + Bu klasör desteklenmiyor Sonraki şarkıya geç Son şarkıya geç Tekrarlama modunu değiştir @@ -160,27 +156,19 @@ Auxio\'nun müzik kitaplığınızı görüntülemek için izne ihtiyacı var Bu görevi yerine getirebilecek bir uygulama bulunamadı Karıştırmayı açın veya kapatın - Klasörü kaldır + Klasörü kaldır Bütün şarkıları karıştır Auxio simgesi Bu sekmeyi taşı Albüm kapağı Arama sorgusunu temizle - Müziğin nereden yükleneceğini yönetin - Mod - Hariç tut - Eklediğiniz klasörlerden müzik yüklenmeyecek. - Dahil et + Müziğin nereden yükleneceğini yönetin Tarih yok Koyu mavi Camgöbeği Deniz mavisi Ek arayüz öğelerinde yuvarlatılmış köşeleri etkinleştirir (Albüm kapaklarının yuvarlatılmış olmasını gerektirir) - Durum geri yüklendi - Önceden kayıtlı çalma durumunu geri getir (varsa) Yuvarlak mod - Çalma durumunu eski haline getir - Durum geri getirelemedi Tekrarda duraklat Müzik yükleniyor Müzik kitaplığı denetleniyor @@ -209,18 +197,15 @@ EP\'ler EP Karışık kasetler - Durum temizlendi Remiksler Film Müzikleri Film Müziği - Yüksek kaliteli + Yüksek kaliteli Katılımcaları gizleyin Yalnızca bir albümde doğrudan adı geçen sanatçıları gösterin (iyi etiketlenmiş kütüphanelerde en iyi sonucu verir) Albüm kapakları Kapalı - Hızlı - Çalma durumunu temizle - Önceki kayıtlı çalma durumunu temizle (varsa) + Hızlı %d Seçili %d sanatçı @@ -235,7 +220,6 @@ Noktalı virgül (;) Artı (+) Ve (&) - Durum kaydedilemedi Çalmayı durdur Viki Müzikleri yeniden tara @@ -248,8 +232,7 @@ Podcast\'ler gibi müzik olmayan ses dosyalarını yok say Uyarı: Bu ayarın kullanılması bazı etiketlerin yanlışlıkla birden fazla değere sahip olarak yorumlanmasına neden olabilir. Bunu, istenmeyen ayırıcı karakterlerin önüne ters eğik çizgi (\\) koyarak çözebilirsiniz. Müzik olmayanları hariç tut - Durum temizlenemedi - ReplayGain stratejisi + ReplayGain yöntemi Bu şarkıyı kuyrukta taşı %1$s, %2$s Müzik ve görüntülerin nasıl yükleneceğini denetleyin @@ -258,19 +241,17 @@ Sesi ve oynatma davranışını yapılandırın Oynatma Kütüphane - Kalıcılık Azalan Uygulamanın temasını ve renklerini değiştirin - Klasörler + Klasörler Arayüz kontrollerini ve davranışını özelleştirin Davranış - Ses yüksekliği dengesi ReplayGain + Ses normaleştirmesi %s için oynatma listesi resmi oynatma listesi çalma listeleri Sıralama yaparken makaleleri yoksay Ada göre sıralarken \"the\" gibi kelimeleri yok sayın (en iyi ingilizce müzikle çalışır) - Yeni bir oynatma listesi oluştur Yeni Oynatma Listesi Sil Yeniden Adlandır @@ -302,4 +283,49 @@ Çalma listesi yeniden adlandırıldı %s silinsin mi\? Geri alınamaz. Üzerinde görünür + Albüm yok + Klasör şeç + Oynatma listesini dışa aktar + Denemeler + ReplayGain albüm ayarlaması + Dosya yolu biçimi + Yakın + Kesin + Oynatma listesi içe aktarıldı + Oynatma listesi dışa aktarıldı + Destekçiler + İsminizin buraya eklenmesi için projeye bağış yapabilirsiniz! + Durmayı hatırla + Şarkı durduğunda sonraki şarkılara geçildiği zaman ve ya şarkı kuyruğunu düzenlerken durmaya devam et + Kapalı + Bu dosyadan bir oynatma listesi yüklenemiyor + Bilinmeyen biçim + İçe aktarılmış oynatma listesi + Bilimeyen albüm + Deneme + Albümleriniz burada gösterilecek. + Oynatma listeleriniz burada gösterilecek. + Türleriniz burada gösterilecek. + ReplayGain parça ayarlaması + Yazar + Bağış yap + Oynatma listesi aktar + Boş oynatma listesi + Dosya yolu + Yeni klasör + Dışa aktar + İçe aktar + Daha fazla + Sanatçılarınız burada gösterilecek. + Şarkılarınız burada gösterilecek. + Oynatmayı başlat + Windows uyumlu yollar kullan + Auxio\'yu önceden kaydedilen durumla başlatır. Eğer kayıtlı bir durum yoksa bütün şarkılar hemen karışık olarak çalmaya başlanacak.\n\nUYARI: Bu servisi kullanırken dikkatli olun. Kapatırsanız ve sonra tekrar kullanmaya çalışırsanız muhtemelen uygulamayı çökertirsiniz. + Az alan kullan + MPEG-4 %s içeriyor + Apple Kayıpsız Ses Kodeği (ALAC) + Geri bildirim + E-posta yolla + Github\'da rapor aç + Bu dosyaya bir oynatma listesi aktarılamıyor \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1429775f1..0d889edf5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -84,9 +84,8 @@ Приховати співавторів Вимкнено Перейти до наступної - Швидкі - Вкажіть звідки слід завантажувати музику - Виключити + Швидкі + Вкажіть звідки слід завантажувати музику Загальна тривалість: %s Мікстейпи Виконавець @@ -100,11 +99,11 @@ Ставити на паузу при повторенні пісні Дата додавання Частота дискретизації - Висока якість + Висока якість Оновити музику Ремікси Мікстейп - Папки з музикою + Папки з музикою Скинути Чорна тема Зміна видимості та порядку вкладок бібліотеки @@ -117,7 +116,6 @@ Переглянути властивості Ігнорувати аудіо файли які не являються музикою, наприклад, подкасти Виключити інші звукові файли - Включити Завантажено альбомів: %d Концертна збірка Мініальбом @@ -137,12 +135,10 @@ Вкладки бібліотеки Автовідтворення в навушниках Режим повторення - Режим Попередній підсилювач ReplayGain Відтворити альбом При відтворенні з бібліотеки Віддавати перевагу альбому, якщо він відтворюється - Стан відтворення очищено Використовувати повністю чорну тему Показувати лише тих виконавців, які безпосередньо зазначені в альбомі (найкраще працює в добре позначених бібліотеках) Увага: Встановлення високих позитивних значень попереднього підсилювача може призвести до спотворення звуку в деяких піснях. @@ -150,13 +146,10 @@ Очистити кеш тегів і повністю перезавантажити музичну бібліотеку (повільніше, але ефективніше) Автоматичне перезавантаження Перезавантажувати бібліотеку при виявленні змін (потрібне постійне сповіщення) - Музика не буде завантажена з вибраних папок. Налаштуйте символи, які позначають кілька значень тегів - Стан відтворення збережено За альбомом За піснею Зміст - Очистити раніше збережений стан відтворення (якщо є) Відстеження змін в музичній бібліотеці… Власна дія для панелі відтворення Регулювання без тегів @@ -165,20 +158,13 @@ Відтворити виконавця Відтворити жанр Перемотати назад перед відтворенням попередньої пісні - Зберегти поточний стан відтворення Пересканувати музику Попередження: Використання цього параметра може призвести до того, що деякі теги будуть неправильно інтерпретовані як такі, що мають кілька значень. Ви можете вирішити це, додавши перед небажаними символами-роздільниками зворотну скісну риску (\\). Кнопка в сповіщенні Багатозначні роздільники - Музика буде завантажена тільки з вибраних папок. - Відновити раніше збережений стан відтворення (якщо є) Регулювання на основі тегів Налаштування ReplayGain - Зберегти стан відтворення - Очистити стан відтворення - Відновити стан відтворення Пісня - Стан відтворення відновлено Перегляд і керування відтворенням музики Перемотайте на початок пісні перед відтворенням попередньої Увімкнути заокруглені кути на додаткових елементах інтерфейсу (потрібно заокруглення обкладинок альбомів) @@ -189,12 +175,11 @@ Плюс (+) Кома (,) Крапка з комою (;) - Ця папка не підтримується + Ця папка не підтримується Невідомий виконавець Лаймовий - Амперсанд (&) - Немає папок - Не вдалось відновити статус відтворення + Амперсанд & + Немає папок Перейти до наступної пісні Ввімкніть або вимкніть перемішування Зупинити відтворення @@ -240,8 +225,6 @@ Дата відсутня Немає пісні Музика не грає - Не вдалось очистити статус відтворення - Не вдалось зберегти статус відтворення Перейти до попередньої пісні Червоний Звук MPEG-4 @@ -253,7 +236,7 @@ Сірий Диск %d Не вдалося завантажити музику - Видалити папку + Видалити папку Розширене кодування звуку (AAC) Звук Matroska %1$s, %2$s @@ -265,18 +248,16 @@ Музика Зображення Відтворення - Вирівнювання гучності (ReplayGain) + Вирівнювання гучності Бібліотека - Стан відтворення Налаштуйте звук і поведінку при відтворенні - Папки + Папки За спаданням Зображення списку відтворення для %s Список відтворення Списки відтворення Інтелектуальне сортування Ігнорування таких слів, як \"the\", або цифр під час сортування за назвою (найкраще працює з англомовною музикою) - Створити новий список відтворення Новий список відтворення Список відтворення %d Додати до списку відтворення @@ -334,4 +315,25 @@ Автор Залишати відтворення/паузу під час пропуску або редагування черги Запам\'ятовувати паузу + Вимкнено + Почати відтворення + Запускати auxio, використовуючи раніше збережений стан. Якщо збережений стан недоступний, усі пісні будуть перетасовані. відтворення розпочнеться негайно. +\n +\nПопередження: будьте обережні при керуванні цією службою, якщо ви закриєте її, а потім спробуєте використовувати знову, ви, ймовірно, призведе до збою застосунка. + Більше + Виберіть папки + Зворотній зв\'язок + Зробіть випуск на GitHub + Надіслати електронний лист + Невідомий альбом + MPEG-4, що містить %s + Невідомий + Аудіокодек Apple без втрат (ALAC) + Нова папка + Економія місця + Ваші пісні відображатимуться тут. + Ваші альбоми відображатимуться тут. + Тут відображатимуться ваші списки відтворення. + Тут відображатимуться ваші жанри. + Тут з’являться ваші виконавці. \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles_core.xml b/app/src/main/res/values-v31/styles_core.xml index 714a9a9fb..d538418f2 100644 --- a/app/src/main/res/values-v31/styles_core.xml +++ b/app/src/main/res/values-v31/styles_core.xml @@ -3,6 +3,7 @@ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 825fd6716..9c56b4c75 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,10 +32,9 @@ 已加入播放队列 查看艺术家 查看专辑 - 已保存播放进度 添加 保存 - 没有文件夹 + 没有文件夹 关于 版本 源代码 @@ -73,8 +72,6 @@ 重复播放前暂停 曲目重复播放前暂停 内容 - 保存播放状态 - 立即保存当前播放状态 刷新音乐 重新加载音乐库,如有可能使用缓存的标签 @@ -82,7 +79,7 @@ 加载音乐失败 Auxio 需要权限来读取音乐库 未找到可以处理此任务的应用 - 该目录不受支持 + 该目录不受支持 在曲库中搜索… @@ -97,7 +94,7 @@ 移动队列曲目 移动该标签 清除搜索队列 - 移除文件夹 + 移除文件夹 Auxio 图标 专辑封面 %s 的专辑封面 @@ -142,11 +139,8 @@ 带有标签的调节项 没有标签的调节项 警告:将前置增益更改为正向高数值或导致某些音轨峰值过高。 - 音乐文件夹 - 管理音乐的加载位置 - 模式 - 排除 - 包含 + 音乐文件夹 + 管理音乐的加载位置 MPEG-1 音频 MPEG-4 音频 Ogg 音频 @@ -163,9 +157,7 @@ 已加载流派数量:%d 总计时长:%s 从展示的项目播放 - 仅从您添加的目录中加载音乐。 从项目详情中选择播放时 - 不会从您添加的目录中加载音乐。 高级音乐编码 (AAC) 已加载专辑数量:%d 音乐加载中 @@ -182,19 +174,12 @@ 采样率 好的 取消 - 清除播放状态 - 清除此前保存的播放状态(如果有) - 无法恢复状态 - 恢复播放状态 - 恢复此前保存的播放状态(如果有) 自动重载 只要发生更改就重新加载曲库(需要持久性通知) 打开队列 正在加载音乐 正在监测曲库 正在监测您的曲库以查找更改… - 已清除状态 - 已恢复状态 EP 专辑 EP 专辑 单曲 @@ -234,17 +219,15 @@ 警告:使用此设置可能会导致某些标签被错误地阐释为具有多个值。要解决这个问题,你可以在不想要的分隔符前加上反斜杠 (\\)。 忽略不是音乐的音频文件,例如播客 排除非音乐 - 高质量 + 高质量 专辑封面 - 快速 + 快速 隐藏协作者 在库中仅显示在出现在“专辑艺术家”标签中的艺术家(在标记良好的库上效果最好) %d 位艺术家 - 无法保存状态 - 无法清除状态 重新扫描音乐 清除标签缓存并完全重新加载音乐库(更慢,但更完整) 选中了 %d 首 @@ -253,15 +236,14 @@ %1$s, %2$s 重置 行为 - 回放增益 - 持久性 + 音量正常化 更改应用的主题和颜色 定制用户界面操控和行为 控制音乐和图片加载方式 图片 播放 曲库 - 文件夹 + 文件夹 音乐 配置声音和播放行为 降序 @@ -269,8 +251,7 @@ 播放列表 %s 的播放列表图片 排序时忽略冠词 - 按名称排序时忽略类似“the”这样的冠词(对英文歌曲的效果最好) - 创建新的播放列表 + 正确排列以数字或像是 \"the\" 这样的单词开头的名称(对英文歌曲效果最好) 新建播放列表 播放列表 %d 已创建播放列表 @@ -328,4 +309,25 @@ 要在此添加您的名字请给项目捐款! 跳过或编辑队列时保留播放/暂停状态 记住暂停状态 + 关闭 + 开始播放 + 使用之前使用的状态启动 Auxio。如果没有可用的已保存状态,将打乱所有歌曲的顺序。播放将立刻开始。 +\n +\n警告:请小心控制此服务,如果将其关闭然后试图再次使用可能造成应用崩溃。 + 更多 + 在 GithHub 上开 issue + 发送电子邮件 + 反馈 + 选择文件夹 + 未知专辑 + 含 %s 的 MPEG-4 + Apple 无损音频编解码器(ALAC) + 未知 + 新文件夹 + 节省空间 + 歌曲会出现在此处。 + 艺术家会出现在此处。 + 流派会出现在此处。 + 专辑会出现在此处。 + 播放列表会出现在此处。 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c59791d5c..45407a509 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -7,7 +7,7 @@ 演出者 專輯 歌曲 - 歌曲 + 所有歌曲 搜尋 篩選器 全部 @@ -73,4 +73,95 @@ 單曲 單曲 在更多的用戶界面元素上啟用圓角(需要專輯封面也要設定圓角) - \ No newline at end of file + 更多 + 演出者 + 新播放隊列 + 匯入播放隊列 + 路徑 + 路徑樣式 + 支援者 + 已匯入播放隊列 + 已匯出播放隊列 + 絕對 + 更多 + 錯誤信息 + 已複製 + 查看和控制音樂回放 + 歌曲屬性 + 隨機 + 歌曲 + 實況單曲 + 重新混音單曲 + 使用與Windows相容的路徑 + 已匯入的播放隊列 + 查看屬性 + 添加到播放隊列 + 播放隊列 + 播放隊列 + 刪除 + 歌曲計數器 + Dj混音 + 刪除播放隊列? + 降序排列 + 報告 + 實況專輯 + 重新混音專輯 + 實況 + 重新混音 + 日期 + 已重新命名播放隊列 + 已刪除播放隊列 + 重新命名 + 重新命名播放隊列 + 編輯 + 名稱 + 碟片 + 音軌 + 添加日期 + 排序依據 + 等化器 + 分享 + 隨機全部 + 取消 + 已建立播放隊列 + 開始回放 + 匯入 + 匯出 + 匯出播放隊列 + 空播放隊列 + 查看 + 回報 + 正在監視你的音樂資料庫更改… + 自定義回放條動作 + 自定義通知動作 + 行為 + 控制如何載入音樂和圖片 + 圖片 + 關閉 + 配置聲音和回放行為 + 回放 + 關閉 + 偏好的音軌 + 捐贈此專案以將你的名字添加到此處! + 耳機自動播放 + 智慧排序 + 你的專輯將顯示在這裡. + 強制使用方形專輯封面 + 自動重新載入 + 多參數分隔符 + 你的歌曲將顯示在這裡. + 你的演出者將顯示在這裡. + 你的類型將顯示在這裡. + 你的播放隊列將顯示在這裡. + 忽略不是音訊的檔案比如博客 + 已添加到播放隊列 + 圓角模式 + 排除非音樂 + 音樂 + 自定義使用者介面控制和行為 + 更改媒體庫標籤的可見性和排序 + 儲存空間 + 記住暫停 + 媒體庫標籤 + 更改應用程式的主題和顏色 + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index a320f9ea5..c3558e528 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -11,12 +11,8 @@ 100 - - - - - - + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index c1f8a7371..000000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,402 +0,0 @@ - - - - #80000000 - - - #01ffffff - - - @color/material_dynamic_primary95 - @color/material_dynamic_neutral20 - @color/material_dynamic_neutral95 - - #BC1714 - #FFFFFF - #FFDAD3 - #410001 - #FFB4A8 - #775652 - #FFFFFF - #FFDAD4 - #2C1512 - #715C2E - #FFFFFF - #FCDFA6 - #261A00 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FCFCFC - #211A19 - #F4DDDA - #534341 - #362F2E - #FBEEEC - - #BC0049 - #FFFFFF - #FFD9DF - #400013 - #FFB2C0 - #76565B - #FFFFFF - #FFD9DE - #2B1519 - #795831 - #FFFFFF - #FFDDB8 - #2C1700 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FCFCFC - #201A1B - #F4DDDF - #524345 - #362F30 - #FAEEEE - - #9A25AE - #FFFFFF - #FFD5FF - #350040 - #FBAAFF - #6B586B - #FFFFFF - #F5DBF2 - #251626 - #82524A - #FFFFFF - #FFDAD2 - #32110C - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FCFCFC - #1E1A1D - #ECDEE8 - #4D444C - #332F32 - #F7EEF3 - - #6F43BF - #FFFFFF - #ECDCFF - #25005A - #D4BAFF - #635B70 - #FFFFFF - #E9DEF7 - #1F182B - #7F525E - #FFFFFF - #FFD9E2 - #32101B - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FFFBFD - #1D1B1F - #E7E0EB - #49454E - #323033 - #F5EFF4 - - #4355B9 - #FFFFFF - #DDE0FF - #000D61 - #B9C3FF - #5B5D71 - #FFFFFF - #E0E1FA - #171A2C - #77536D - #FFFFFF - #FFD7F3 - #2D1228 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FEFBFF - #1B1B1F - #E3E1EC - #46464F - #303034 - #F3F0F5 - - #0061A6 - #FFFFFF - #D0E4FF - #001D36 - #9CCAFF - #535F70 - #FFFFFF - #D6E3F7 - #101C2B - #6B5778 - #FFFFFF - #F3DAFF - #251432 - #BA1B1B - #FFDAD4 - #FFFFFF - #410001 - #FDFCFF - #1B1B1B - #DFE2EB - #42474E - #2F3033 - #F1F0F4 - - #006684 - #FFFFFF - #BAE9FF - #001F2A - #62D3FF - #4D616B - #FFFFFF - #D0E6F2 - #081E27 - #5D5B7E - #FFFFFF - #E3DFFF - #191836 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FBFCFE - #191C1E - #DCE4E9 - #40484C - #2E3133 - #F0F1F3 - - #006877 - #FFFFFF - #9CEFFF - #001F25 - #44D8F1 - #4A6267 - #FFFFFF - #CDE7ED - #051F23 - #545D7D - #FFFFFF - #DAE1FF - #101A37 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FBFDFE - #191C1D - #DBE4E6 - #3F484A - #2D3132 - #EFF1F2 - - #006A5F - #FFFFFF - #74F7E5 - #00201C - #53DBC9 - #4A635F - #FFFFFF - #CDE8E2 - #05201C - #466179 - #FFFFFF - #CBE5FF - #001D31 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FAFDFA - #191C1B - #DBE5E2 - #3F4947 - #2D3130 - #EFF1EF - - #006E17 - #FFFFFF - #93F990 - #002203 - #78DC77 - #52634F - #FFFFFF - #D5E8CE - #101F0F - #38656A - #FFFFFF - #BCEBF0 - #001F23 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FCFDF6 - #1A1C19 - #DEE5D8 - #424840 - #2F312D - #F0F1EB - - #3C6A00 - #FFFFFF - #B9F475 - #0E2000 - #9ED75C - #58624A - #FFFFFF - #DBE7C7 - #151E0B - #386663 - #FFFFFF - #BCECE8 - #00201E - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FDFCF5 - #1A1C17 - #E1E4D6 - #44483D - #30312D - #F2F1EA - - #5A6400 - #FFFFFF - #DEED49 - #1A1E00 - #C1D02C - #5E6044 - #FFFFFF - #E4E5C1 - #1B1D07 - #3C665A - #FFFFFF - #BEECDC - #002019 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FFFCF3 - #1C1C17 - #E5E3D2 - #47473B - #31312B - #F3F0E8 - - #FFB300 - #FFFFFF - #FFDF99 - #261A00 - #FABD00 - #6B5C3F - #FFFFFF - #F4E0BB - #241A04 - #496546 - #FFFFFF - #CCEBC5 - #082009 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FFFBF8 - #1E1B16 - #EDE1CF - #4D4639 - #34302A - #F8F0E7 - - #FB8C00 - #FFFFFF - #FFDCBB - #2D1600 - #FFB86D - #735A42 - #FFFFFF - #FEDCBE - #291806 - #586339 - #FFFFFF - #DCE8B4 - #161E01 - #BA1B1B - #FFDAD4 - #FFFFFF - #410001 - #FCFCFC - #1F1B17 - #F2DFD1 - #51453A - #352F2A - #FAEFE7 - - #77574C - #FFFFFF - #FFDBCD - #2C160D - #E7BEB0 - #9A4523 - #FFFFFF - #FFDBCD - #380C00 - #695E2F - #FFFFFF - #F1E2A7 - #221B00 - #BA1B1B - #FFFFFF - #FFDAD4 - #410001 - #FCFCFC - #201A18 - #F5DED6 - #52433E - #362F2D - #FCEEEA - - #757575 - #FFFFFF - #EEEEEE - #E0E0E0 - #212121 - #4D4D4D - #FFFFFF - #D0D0D0 - #080808 - #5D5D5D - #FFFFFF - #E3E3E3 - #191919 - #BA1B1B - #FFFFFF - #FFDADA - #410000 - #fafafa - #191919 - #DCDCDC - #484848 - #1f1f1f - #F0F0F0 - - @color/m3_ref_palette_dynamic_primary40 - \ No newline at end of file diff --git a/app/src/main/res/values/colors_android.xml b/app/src/main/res/values/colors_android.xml new file mode 100644 index 000000000..f107ae056 --- /dev/null +++ b/app/src/main/res/values/colors_android.xml @@ -0,0 +1,18 @@ + + + + #80000000 + + + #01ffffff + + + @color/material_dynamic_primary95 + @color/material_dynamic_neutral20 + @color/material_dynamic_neutral95 + + @color/m3_ref_palette_dynamic_primary40 + \ No newline at end of file diff --git a/app/src/main/res/values/colors_ui.xml b/app/src/main/res/values/colors_ui.xml new file mode 100644 index 000000000..984e35604 --- /dev/null +++ b/app/src/main/res/values/colors_ui.xml @@ -0,0 +1,2274 @@ + + + #904A42 + #FFFFFF + #FFDAD5 + #3B0906 + #775652 + #FFFFFF + #FFDAD5 + #2C1512 + #705C2E + #FFFFFF + #FCDFA6 + #261A00 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF8F7 + #231918 + #FFF8F7 + #231918 + #F5DDDA + #534341 + #857370 + #D8C2BE + #000000 + #392E2C + #FFEDEA + #FFB4A9 + #FFDAD5 + #3B0906 + #FFB4A9 + #73342C + #FFDAD5 + #2C1512 + #E7BDB7 + #5D3F3B + #FCDFA6 + #261A00 + #DFC38C + #574419 + #E8D6D3 + #FFF8F7 + #FFFFFF + #FFF0EE + #FCEAE7 + #F7E4E1 + #F1DEDC + #6E3028 + #FFFFFF + #AA6056 + #FFFFFF + #593B37 + #FFFFFF + #8F6C67 + #FFFFFF + #534015 + #FFFFFF + #887242 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF8F7 + #231918 + #FFF8F7 + #231918 + #F5DDDA + #4F3F3D + #6C5B59 + #897674 + #000000 + #392E2C + #FFEDEA + #FFB4A9 + #AA6056 + #FFFFFF + #8D483F + #FFFFFF + #8F6C67 + #FFFFFF + #745450 + #FFFFFF + #887242 + #FFFFFF + #6E592C + #FFFFFF + #E8D6D3 + #FFF8F7 + #FFFFFF + #FFF0EE + #FCEAE7 + #F7E4E1 + #F1DEDC + #44100B + #FFFFFF + #6E3028 + #FFFFFF + #341C18 + #FFFFFF + #593B37 + #FFFFFF + #2E2000 + #FFFFFF + #534015 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF8F7 + #231918 + #FFF8F7 + #000000 + #F5DDDA + #2E211F + #4F3F3D + #4F3F3D + #000000 + #392E2C + #FFFFFF + #FFE7E3 + #6E3028 + #FFFFFF + #521A15 + #FFFFFF + #593B37 + #FFFFFF + #402622 + #FFFFFF + #534015 + #FFFFFF + #3A2A02 + #FFFFFF + #E8D6D3 + #FFF8F7 + #FFFFFF + #FFF0EE + #FCEAE7 + #F7E4E1 + #F1DEDC + + #8E4956 + #FFFFFF + #FFD9DD + #3B0715 + #76565A + #FFFFFF + #FFD9DD + #2C1519 + #795831 + #FFFFFF + #FFDDB9 + #2B1700 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF8F7 + #22191A + #FFF8F7 + #22191A + #F3DDDF + #524345 + #847375 + #D6C2C3 + #000000 + #382E2F + #FEEDEE + #FFB2BD + #FFD9DD + #3B0715 + #FFB2BD + #72333F + #FFD9DD + #2C1519 + #E5BDC1 + #5C3F43 + #FFDDB9 + #2B1700 + #EABF8F + #5E411C + #E7D6D7 + #FFF8F7 + #FFFFFF + #FFF0F1 + #FBEAEB + #F6E4E5 + #F0DEDF + #6D2F3B + #FFFFFF + #A85F6B + #FFFFFF + #583B3F + #FFFFFF + #8D6C70 + #FFFFFF + #5A3D18 + #FFFFFF + #916E45 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF8F7 + #22191A + #FFF8F7 + #22191A + #F3DDDF + #4E3F41 + #6B5B5D + #887678 + #000000 + #382E2F + #FEEDEE + #FFB2BD + #A85F6B + #FFFFFF + #8B4753 + #FFFFFF + #8D6C70 + #FFFFFF + #735458 + #FFFFFF + #916E45 + #FFFFFF + #76562F + #FFFFFF + #E7D6D7 + #FFF8F7 + #FFFFFF + #FFF0F1 + #FBEAEB + #F6E4E5 + #F0DEDF + #430E1B + #FFFFFF + #6D2F3B + #FFFFFF + #331B1F + #FFFFFF + #583B3F + #FFFFFF + #341D00 + #FFFFFF + #5A3D18 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF8F7 + #22191A + #FFF8F7 + #000000 + #F3DDDF + #2D2122 + #4E3F41 + #4E3F41 + #000000 + #382E2F + #FFFFFF + #FFE6E8 + #6D2F3B + #FFFFFF + #511926 + #FFFFFF + #583B3F + #FFFFFF + #3F2629 + #FFFFFF + #5A3D18 + #FFFFFF + #412705 + #FFFFFF + #E7D6D7 + #FFF8F7 + #FFFFFF + #FFF0F1 + #FBEAEB + #F6E4E5 + #F0DEDF + + #7B4E7F + #FFFFFF + #FFD6FE + #310937 + #6B586B + #FFFFFF + #F4DBF1 + #251626 + #82524A + #FFFFFF + #FFDAD4 + #33110C + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF7FA + #1F1A1F + #FFF7FA + #1F1A1F + #ECDFE8 + #4D444C + #7F747D + #D0C3CC + #000000 + #352F34 + #F9EEF5 + #EBB5ED + #FFD6FE + #310937 + #EBB5ED + #613766 + #F4DBF1 + #251626 + #D7BFD5 + #534153 + #FFDAD4 + #33110C + #F6B8AD + #673B34 + #E2D7DE + #FFF7FA + #FFFFFF + #FCF0F7 + #F6EBF2 + #F0E5EC + #EBDFE6 + #5D3362 + #FFFFFF + #936497 + #FFFFFF + #4F3D4F + #FFFFFF + #826E82 + #FFFFFF + #623730 + #FFFFFF + #9B685F + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF7FA + #1F1A1F + #FFF7FA + #1F1A1F + #ECDFE8 + #494048 + #665C64 + #827880 + #000000 + #352F34 + #F9EEF5 + #EBB5ED + #936497 + #FFFFFF + #794C7D + #FFFFFF + #826E82 + #FFFFFF + #695668 + #FFFFFF + #9B685F + #FFFFFF + #7F5048 + #FFFFFF + #E2D7DE + #FFF7FA + #FFFFFF + #FCF0F7 + #F6EBF2 + #F0E5EC + #EBDFE6 + #38113F + #FFFFFF + #5D3362 + #FFFFFF + #2C1D2D + #FFFFFF + #4F3D4F + #FFFFFF + #3B1812 + #FFFFFF + #623730 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF7FA + #1F1A1F + #FFF7FA + #000000 + #ECDFE8 + #292229 + #494048 + #494048 + #000000 + #352F34 + #FFFFFF + #FFE4FC + #5D3362 + #FFFFFF + #441C4A + #FFFFFF + #4F3D4F + #FFFFFF + #372738 + #FFFFFF + #623730 + #FFFFFF + #48221C + #FFFFFF + #E2D7DE + #FFF7FA + #FFFFFF + #FCF0F7 + #F6EBF2 + #F0E5EC + #EBDFE6 + + #68548E + #FFFFFF + #EBDDFF + #230F46 + #635B70 + #FFFFFF + #E9DEF8 + #1F182B + #7E525D + #FFFFFF + #FFD9E1 + #31101B + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FEF7FF + #1D1B20 + #FEF7FF + #1D1B20 + #E7E0EB + #49454E + #7A757F + #CBC4CF + #000000 + #322F35 + #F5EFF7 + #D3BCFD + #EBDDFF + #230F46 + #D3BCFD + #4F3D74 + #E9DEF8 + #1F182B + #CDC2DB + #4B4358 + #FFD9E1 + #31101B + #F0B7C5 + #643B46 + #DED8E0 + #FEF7FF + #FFFFFF + #F8F1FA + #F2ECF4 + #EDE6EE + #E7E0E8 + #4B3970 + #FFFFFF + #7F6AA5 + #FFFFFF + #473F54 + #FFFFFF + #797187 + #FFFFFF + #5F3742 + #FFFFFF + #976774 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FEF7FF + #1D1B20 + #FEF7FF + #1D1B20 + #E7E0EB + #45414A + #625D67 + #7E7983 + #000000 + #322F35 + #F5EFF7 + #D3BCFD + #7F6AA5 + #FFFFFF + #65528B + #FFFFFF + #797187 + #FFFFFF + #60586E + #FFFFFF + #976774 + #FFFFFF + #7C4F5B + #FFFFFF + #DED8E0 + #FEF7FF + #FFFFFF + #F8F1FA + #F2ECF4 + #EDE6EE + #E7E0E8 + #2A164D + #FFFFFF + #4B3970 + #FFFFFF + #251F32 + #FFFFFF + #473F54 + #FFFFFF + #391722 + #FFFFFF + #5F3742 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FEF7FF + #1D1B20 + #FEF7FF + #000000 + #E7E0EB + #26222B + #45414A + #45414A + #000000 + #322F35 + #FFFFFF + #F3E8FF + #4B3970 + #FFFFFF + #352258 + #FFFFFF + #473F54 + #FFFFFF + #30293D + #FFFFFF + #5F3742 + #FFFFFF + #46212C + #FFFFFF + #DED8E0 + #FEF7FF + #FFFFFF + #F8F1FA + #F2ECF4 + #EDE6EE + #E7E0E8 + + #515B92 + #FFFFFF + #DEE0FF + #0B154B + #5B5D72 + #FFFFFF + #E0E1F9 + #181A2C + #77536D + #FFFFFF + #FFD7F1 + #2D1228 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FBF8FF + #1B1B21 + #FBF8FF + #1B1B21 + #E3E1EC + #46464F + #767680 + #C7C5D0 + #000000 + #303036 + #F2EFF7 + #BAC3FF + #DEE0FF + #0B154B + #BAC3FF + #394379 + #E0E1F9 + #181A2C + #C3C5DD + #434659 + #FFD7F1 + #2D1228 + #E6BAD7 + #5D3C55 + #DBD9E0 + #FBF8FF + #FFFFFF + #F5F2FA + #EFEDF4 + #E9E7EF + #E4E1E9 + #353F74 + #FFFFFF + #6871AA + #FFFFFF + #3F4255 + #FFFFFF + #717389 + #FFFFFF + #593851 + #FFFFFF + #8E6984 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FBF8FF + #1B1B21 + #FBF8FF + #1B1B21 + #E3E1EC + #42424B + #5E5E67 + #7A7A83 + #000000 + #303036 + #F2EFF7 + #BAC3FF + #6871AA + #FFFFFF + #4F5890 + #FFFFFF + #717389 + #FFFFFF + #585B6F + #FFFFFF + #8E6984 + #FFFFFF + #74516B + #FFFFFF + #DBD9E0 + #FBF8FF + #FFFFFF + #F5F2FA + #EFEDF4 + #E9E7EF + #E4E1E9 + #131D52 + #FFFFFF + #353F74 + #FFFFFF + #1E2133 + #FFFFFF + #3F4255 + #FFFFFF + #34182F + #FFFFFF + #593851 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FBF8FF + #1B1B21 + #FBF8FF + #000000 + #E3E1EC + #23232B + #42424B + #42424B + #000000 + #303036 + #FFFFFF + #EAEAFF + #353F74 + #FFFFFF + #1E285D + #FFFFFF + #3F4255 + #FFFFFF + #292C3E + #FFFFFF + #593851 + #FFFFFF + #40233A + #FFFFFF + #DBD9E0 + #FBF8FF + #FFFFFF + #F5F2FA + #EFEDF4 + #E9E7EF + #E4E1E9 + + #37618E + #FFFFFF + #D2E4FF + #001C37 + #535F70 + #FFFFFF + #D7E3F8 + #101C2B + #6B5778 + #FFFFFF + #F3DAFF + #251431 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F8F9FF + #191C20 + #F8F9FF + #191C20 + #DFE2EB + #43474E + #73777F + #C3C6CF + #000000 + #2E3135 + #EFF0F7 + #A1C9FD + #D2E4FF + #001C37 + #A1C9FD + #1B4975 + #D7E3F8 + #101C2B + #BBC7DB + #3C4858 + #F3DAFF + #251431 + #D7BDE4 + #533F5F + #D8DAE0 + #F8F9FF + #FFFFFF + #F2F3FA + #ECEEF4 + #E7E8EE + #E1E2E8 + #164571 + #FFFFFF + #4F77A6 + #FFFFFF + #384454 + #FFFFFF + #697587 + #FFFFFF + #4F3B5B + #FFFFFF + #826D8F + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F8F9FF + #191C20 + #F8F9FF + #191C20 + #DFE2EB + #3F434A + #5B5F67 + #777B83 + #000000 + #2E3135 + #EFF0F7 + #A1C9FD + #4F77A6 + #FFFFFF + #345E8C + #FFFFFF + #697587 + #FFFFFF + #515D6E + #FFFFFF + #826D8F + #FFFFFF + #695475 + #FFFFFF + #D8DAE0 + #F8F9FF + #FFFFFF + #F2F3FA + #ECEEF4 + #E7E8EE + #E1E2E8 + #002342 + #FFFFFF + #164571 + #FFFFFF + #172332 + #FFFFFF + #384454 + #FFFFFF + #2C1B38 + #FFFFFF + #4F3B5B + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F8F9FF + #191C20 + #F8F9FF + #000000 + #DFE2EB + #20242B + #3F434A + #3F434A + #000000 + #2E3135 + #FFFFFF + #E2EDFF + #164571 + #FFFFFF + #002E53 + #FFFFFF + #384454 + #FFFFFF + #222E3D + #FFFFFF + #4F3B5B + #FFFFFF + #372543 + #FFFFFF + #D8DAE0 + #F8F9FF + #FFFFFF + #F2F3FA + #ECEEF4 + #E7E8EE + #E1E2E8 + + #136682 + #FFFFFF + #BEE9FF + #001F2A + #4D616C + #FFFFFF + #D0E6F2 + #081E27 + #5D5B7D + #FFFFFF + #E3DFFF + #1A1836 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F6FAFE + #171C1F + #F6FAFE + #171C1F + #DCE4E9 + #40484C + #70787D + #C0C8CD + #000000 + #2C3134 + #EDF1F5 + #8BD0F0 + #BEE9FF + #001F2A + #8BD0F0 + #004D64 + #D0E6F2 + #081E27 + #B4CAD6 + #354A54 + #E3DFFF + #1A1836 + #C6C2EA + #454364 + #D6DBDE + #F6FAFE + #FFFFFF + #F0F4F8 + #EAEEF2 + #E4E9EC + #DFE3E7 + #00495F + #FFFFFF + #347D9A + #FFFFFF + #314650 + #FFFFFF + #637882 + #FFFFFF + #413F60 + #FFFFFF + #747195 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F6FAFE + #171C1F + #F6FAFE + #171C1F + #DCE4E9 + #3C4448 + #586065 + #747C81 + #000000 + #2C3134 + #EDF1F5 + #8BD0F0 + #347D9A + #FFFFFF + #0D6480 + #FFFFFF + #637882 + #FFFFFF + #4A5F69 + #FFFFFF + #747195 + #FFFFFF + #5B587B + #FFFFFF + #D6DBDE + #F6FAFE + #FFFFFF + #F0F4F8 + #EAEEF2 + #E4E9EC + #DFE3E7 + #002633 + #FFFFFF + #00495F + #FFFFFF + #10252E + #FFFFFF + #314650 + #FFFFFF + #211E3D + #FFFFFF + #413F60 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F6FAFE + #171C1F + #F6FAFE + #000000 + #DCE4E9 + #1D2529 + #3C4448 + #3C4448 + #000000 + #2C3134 + #FFFFFF + #D5F0FF + #00495F + #FFFFFF + #003141 + #FFFFFF + #314650 + #FFFFFF + #1B2F39 + #FFFFFF + #413F60 + #FFFFFF + #2B2949 + #FFFFFF + #D6DBDE + #F6FAFE + #FFFFFF + #F0F4F8 + #EAEEF2 + #E4E9EC + #DFE3E7 + + #006877 + #FFFFFF + #A4EEFF + #001F25 + #4B6268 + #FFFFFF + #CDE7ED + #051F24 + #545D7E + #FFFFFF + #DCE1FF + #111A37 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F5FAFC + #171D1E + #F5FAFC + #171D1E + #DBE4E7 + #3F484B + #6F797B + #BFC8CB + #000000 + #2B3133 + #ECF2F3 + #83D2E4 + #A4EEFF + #001F25 + #83D2E4 + #004E5A + #CDE7ED + #051F24 + #B2CBD1 + #334A50 + #DCE1FF + #111A37 + #BDC5EB + #3D4565 + #D5DBDD + #F5FAFC + #FFFFFF + #EFF4F6 + #E9EFF0 + #E3E9EB + #DEE3E5 + #004A55 + #FFFFFF + #277F8F + #FFFFFF + #2F464C + #FFFFFF + #60797E + #FFFFFF + #394261 + #FFFFFF + #6B7395 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F5FAFC + #171D1E + #F5FAFC + #171D1E + #DBE4E7 + #3B4447 + #576163 + #737C7F + #000000 + #2B3133 + #ECF2F3 + #83D2E4 + #277F8F + #FFFFFF + #006574 + #FFFFFF + #60797E + #FFFFFF + #486065 + #FFFFFF + #6B7395 + #FFFFFF + #525B7B + #FFFFFF + #D5DBDD + #F5FAFC + #FFFFFF + #EFF4F6 + #E9EFF0 + #E3E9EB + #DEE3E5 + #00262D + #FFFFFF + #004A55 + #FFFFFF + #0D252A + #FFFFFF + #2F464C + #FFFFFF + #18213E + #FFFFFF + #394261 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F5FAFC + #171D1E + #F5FAFC + #000000 + #DBE4E7 + #1D2527 + #3B4447 + #3B4447 + #000000 + #2B3133 + #FFFFFF + #C6F4FF + #004A55 + #FFFFFF + #00323A + #FFFFFF + #2F464C + #FFFFFF + #183035 + #FFFFFF + #394261 + #FFFFFF + #232B49 + #FFFFFF + #D5DBDD + #F5FAFC + #FFFFFF + #EFF4F6 + #E9EFF0 + #E3E9EB + #DEE3E5 + + #006B5F + #FFFFFF + #9EF2E3 + #00201C + #4A635E + #FFFFFF + #CCE8E2 + #06201C + #456179 + #FFFFFF + #CCE5FF + #001E31 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F4FBF8 + #161D1B + #F4FBF8 + #161D1B + #DAE5E1 + #3F4946 + #6F7976 + #BEC9C5 + #000000 + #2B3230 + #ECF2EF + #82D5C7 + #9EF2E3 + #00201C + #82D5C7 + #005048 + #CCE8E2 + #06201C + #B1CCC6 + #334B47 + #CCE5FF + #001E31 + #ADCAE5 + #2D4960 + #D5DBD9 + #F4FBF8 + #FFFFFF + #EFF5F2 + #E9EFEC + #E3EAE7 + #DDE4E1 + #004C44 + #FFFFFF + #298176 + #FFFFFF + #2F4743 + #FFFFFF + #607A74 + #FFFFFF + #29465C + #FFFFFF + #5B7890 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F4FBF8 + #161D1B + #F4FBF8 + #161D1B + #DAE5E1 + #3B4543 + #57615F + #737D7A + #000000 + #2B3230 + #ECF2EF + #82D5C7 + #298176 + #FFFFFF + #00685D + #FFFFFF + #607A74 + #FFFFFF + #48615C + #FFFFFF + #5B7890 + #FFFFFF + #435F77 + #FFFFFF + #D5DBD9 + #F4FBF8 + #FFFFFF + #EFF5F2 + #E9EFEC + #E3EAE7 + #DDE4E1 + #002823 + #FFFFFF + #004C44 + #FFFFFF + #0D2622 + #FFFFFF + #2F4743 + #FFFFFF + #02243A + #FFFFFF + #29465C + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F4FBF8 + #161D1B + #F4FBF8 + #000000 + #DAE5E1 + #1C2624 + #3B4543 + #3B4543 + #000000 + #2B3230 + #FFFFFF + #A8FCED + #004C44 + #FFFFFF + #00332D + #FFFFFF + #2F4743 + #FFFFFF + #18312D + #FFFFFF + #29465C + #FFFFFF + #102F45 + #FFFFFF + #D5DBD9 + #F4FBF8 + #FFFFFF + #EFF5F2 + #E9EFEC + #E3EAE7 + #DDE4E1 + + #3C6838 + #FFFFFF + #BDF0B3 + #002203 + #53634E + #FFFFFF + #D6E8CE + #111F0F + #38656A + #FFFFFF + #BCEBF0 + #002022 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F7FBF1 + #191D17 + #F7FBF1 + #191D17 + #DEE5D8 + #424940 + #73796F + #C2C8BD + #000000 + #2D322B + #EFF2E9 + #A2D399 + #BDF0B3 + #002203 + #A2D399 + #255023 + #D6E8CE + #111F0F + #BACCB3 + #3B4B38 + #BCEBF0 + #002022 + #A0CFD4 + #1E4D52 + #D8DBD2 + #F7FBF1 + #FFFFFF + #F2F5EB + #ECEFE6 + #E6E9E0 + #E0E4DA + #214C1F + #FFFFFF + #527F4D + #FFFFFF + #384734 + #FFFFFF + #697964 + #FFFFFF + #1A494E + #FFFFFF + #4F7C80 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F7FBF1 + #191D17 + #F7FBF1 + #191D17 + #DEE5D8 + #3E453C + #5B6157 + #767D72 + #000000 + #2D322B + #EFF2E9 + #A2D399 + #527F4D + #FFFFFF + #3A6636 + #FFFFFF + #697964 + #FFFFFF + #50604C + #FFFFFF + #4F7C80 + #FFFFFF + #366367 + #FFFFFF + #D8DBD2 + #F7FBF1 + #FFFFFF + #F2F5EB + #ECEFE6 + #E6E9E0 + #E0E4DA + #002904 + #FFFFFF + #214C1F + #FFFFFF + #172615 + #FFFFFF + #384734 + #FFFFFF + #00272A + #FFFFFF + #1A494E + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F7FBF1 + #191D17 + #F7FBF1 + #000000 + #DEE5D8 + #20261E + #3E453C + #3E453C + #000000 + #2D322B + #FFFFFF + #C6FABC + #214C1F + #FFFFFF + #07350B + #FFFFFF + #384734 + #FFFFFF + #22301F + #FFFFFF + #1A494E + #FFFFFF + #003236 + #FFFFFF + #D8DBD2 + #F7FBF1 + #FFFFFF + #F2F5EB + #ECEFE6 + #E6E9E0 + #E0E4DA + + #4A672D + #FFFFFF + #CBEDA5 + #0E2000 + #57624A + #FFFFFF + #DBE7C8 + #151E0B + #386664 + #FFFFFF + #BBECE8 + #00201F + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F9FAEF + #1A1C16 + #F9FAEF + #1A1C16 + #E1E4D5 + #44483D + #75796C + #C4C8BA + #000000 + #2F312A + #F0F2E6 + #B0D18B + #CBEDA5 + #0E2000 + #B0D18B + #334E17 + #DBE7C8 + #151E0B + #BFCBAD + #404A34 + #BBECE8 + #00201F + #A0CFCC + #1F4E4C + #D9DBD0 + #F9FAEF + #FFFFFF + #F3F5E9 + #EDEFE4 + #E8E9DE + #E2E3D8 + #304A13 + #FFFFFF + #607D41 + #FFFFFF + #3C4630 + #FFFFFF + #6D785F + #FFFFFF + #1A4A48 + #FFFFFF + #4F7C7A + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F9FAEF + #1A1C16 + #F9FAEF + #1A1C16 + #E1E4D5 + #40443A + #5C6155 + #787C70 + #000000 + #2F312A + #F0F2E6 + #B0D18B + #607D41 + #FFFFFF + #48642A + #FFFFFF + #6D785F + #FFFFFF + #556048 + #FFFFFF + #4F7C7A + #FFFFFF + #366361 + #FFFFFF + #D9DBD0 + #F9FAEF + #FFFFFF + #F3F5E9 + #EDEFE4 + #E8E9DE + #E2E3D8 + #132700 + #FFFFFF + #304A13 + #FFFFFF + #1C2512 + #FFFFFF + #3C4630 + #FFFFFF + #002726 + #FFFFFF + #1A4A48 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F9FAEF + #1A1C16 + #F9FAEF + #000000 + #E1E4D5 + #21251C + #40443A + #40443A + #000000 + #2F312A + #FFFFFF + #D5F7AE + #304A13 + #FFFFFF + #1A3300 + #FFFFFF + #3C4630 + #FFFFFF + #26301B + #FFFFFF + #1A4A48 + #FFFFFF + #003331 + #FFFFFF + #D9DBD0 + #F9FAEF + #FFFFFF + #F3F5E9 + #EDEFE4 + #E8E9DE + #E2E3D8 + + #5B631E + #FFFFFF + #E0E995 + #1A1E00 + #5E6144 + #FFFFFF + #E3E5C1 + #1B1D07 + #3B665B + #FFFFFF + #BEECDD + #002019 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FCFAED + #1B1C14 + #FCFAED + #1B1C14 + #E4E3D2 + #47483B + #78786A + #C8C7B7 + #000000 + #313128 + #F3F1E4 + #C3CD7B + #E0E995 + #1A1E00 + #C3CD7B + #434B05 + #E3E5C1 + #1B1D07 + #C7C9A7 + #46492F + #BEECDD + #002019 + #A2D0C1 + #224E43 + #DCDACE + #FCFAED + #FFFFFF + #F6F4E7 + #F0EEE1 + #EAE9DC + #E5E3D6 + #404701 + #FFFFFF + #717A32 + #FFFFFF + #42452B + #FFFFFF + #747759 + #FFFFFF + #1E4A3F + #FFFFFF + #517D70 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FCFAED + #1B1C14 + #FCFAED + #1B1C14 + #E4E3D2 + #434437 + #5F6052 + #7B7C6D + #000000 + #313128 + #F3F1E4 + #C3CD7B + #717A32 + #FFFFFF + #59611B + #FFFFFF + #747759 + #FFFFFF + #5B5E42 + #FFFFFF + #517D70 + #FFFFFF + #396458 + #FFFFFF + #DCDACE + #FCFAED + #FFFFFF + #F6F4E7 + #F0EEE1 + #EAE9DC + #E5E3D6 + #202500 + #FFFFFF + #404701 + #FFFFFF + #21240D + #FFFFFF + #42452B + #FFFFFF + #002820 + #FFFFFF + #1E4A3F + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FCFAED + #1B1C14 + #FCFAED + #000000 + #E4E3D2 + #24251A + #434437 + #434437 + #000000 + #313128 + #FFFFFF + #E9F39D + #404701 + #FFFFFF + #2A3000 + #FFFFFF + #42452B + #FFFFFF + #2C2E17 + #FFFFFF + #1E4A3F + #FFFFFF + #01332A + #FFFFFF + #DCDACE + #FCFAED + #FFFFFF + #F6F4E7 + #F0EEE1 + #EAE9DC + #E5E3D6 + + #7D570D + #FFFFFF + #FFDEAC + #281900 + #6E5C40 + #FFFFFF + #F8DFBB + #261904 + #4E6542 + #FFFFFF + #D1EABF + #0D2005 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF8F3 + #201B13 + #FFF8F3 + #201B13 + #EFE0CF + #4E4539 + #807567 + #D2C4B4 + #000000 + #362F27 + #FBEFE2 + #F0BE6D + #FFDEAC + #281900 + #F0BE6D + #604100 + #F8DFBB + #261904 + #DBC3A1 + #55442A + #D1EABF + #0D2005 + #B5CEA4 + #374C2C + #E4D8CC + #FFF8F3 + #FFFFFF + #FEF2E5 + #F8ECDF + #F2E6D9 + #ECE1D4 + #5B3D00 + #FFFFFF + #966D24 + #FFFFFF + #504026 + #FFFFFF + #857254 + #FFFFFF + #334829 + #FFFFFF + #647B57 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF8F3 + #201B13 + #FFF8F3 + #201B13 + #EFE0CF + #4A4135 + #685E50 + #84796B + #000000 + #362F27 + #FBEFE2 + #F0BE6D + #966D24 + #FFFFFF + #7A550A + #FFFFFF + #857254 + #FFFFFF + #6B593D + #FFFFFF + #647B57 + #FFFFFF + #4C6240 + #FFFFFF + #E4D8CC + #FFF8F3 + #FFFFFF + #FEF2E5 + #F8ECDF + #F2E6D9 + #ECE1D4 + #301F00 + #FFFFFF + #5B3D00 + #FFFFFF + #2D2009 + #FFFFFF + #504026 + #FFFFFF + #14270B + #FFFFFF + #334829 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF8F3 + #201B13 + #FFF8F3 + #000000 + #EFE0CF + #2A2318 + #4A4135 + #4A4135 + #000000 + #362F27 + #FFFFFF + #FFE9CB + #5B3D00 + #FFFFFF + #3E2900 + #FFFFFF + #504026 + #FFFFFF + #392A12 + #FFFFFF + #334829 + #FFFFFF + #1E3214 + #FFFFFF + #E4D8CC + #FFF8F3 + #FFFFFF + #FEF2E5 + #F8ECDF + #F2E6D9 + #ECE1D4 + + #88511D + #FFFFFF + #FFDCC2 + #2E1500 + #745944 + #FFFFFF + #FFDCC2 + #2A1707 + #5B6237 + #FFFFFF + #E0E7B1 + #191E00 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF8F5 + #221A14 + #FFF8F5 + #221A14 + #F3DFD1 + #51443B + #847469 + #D6C3B6 + #000000 + #382F28 + #FEEEE4 + #FFB77B + #FFDCC2 + #2E1500 + #FFB77B + #6B3A05 + #FFDCC2 + #2A1707 + #E3C0A5 + #5A422E + #E0E7B1 + #191E00 + #C4CB97 + #444A22 + #E7D7CD + #FFF8F5 + #FFFFFF + #FFF1E8 + #FBEBE1 + #F5E5DB + #EFE0D6 + #673702 + #FFFFFF + #A26731 + #FFFFFF + #563E2A + #FFFFFF + #8C6F58 + #FFFFFF + #40461E + #FFFFFF + #72784B + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF8F5 + #221A14 + #FFF8F5 + #221A14 + #F3DFD1 + #4D4037 + #6B5C52 + #87786D + #000000 + #382F28 + #FEEEE4 + #FFB77B + #A26731 + #FFFFFF + #854F1B + #FFFFFF + #8C6F58 + #FFFFFF + #715741 + #FFFFFF + #72784B + #FFFFFF + #596035 + #FFFFFF + #E7D7CD + #FFF8F5 + #FFFFFF + #FFF1E8 + #FBEBE1 + #F5E5DB + #EFE0D6 + #381B00 + #FFFFFF + #673702 + #FFFFFF + #311E0C + #FFFFFF + #563E2A + #FFFFFF + #1F2502 + #FFFFFF + #40461E + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF8F5 + #221A14 + #FFF8F5 + #000000 + #F3DFD1 + #2C2219 + #4D4037 + #4D4037 + #000000 + #382F28 + #FFFFFF + #FFE8D8 + #673702 + #FFFFFF + #472400 + #FFFFFF + #563E2A + #FFFFFF + #3D2816 + #FFFFFF + #40461E + #FFFFFF + #2A2F0A + #FFFFFF + #E7D7CD + #FFF8F5 + #FFFFFF + #FFF1E8 + #FBEBE1 + #F5E5DB + #EFE0D6 + + #604238 + #FFFFFF + #87665A + #FFFFFF + #6B5B55 + #FFFFFF + #F9E2DB + #574742 + #4F4A2D + #FFFFFF + #746E4E + #FFFFFF + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FFF8F6 + #1E1B1A + #FFF8F6 + #1E1B1A + #F1DFD9 + #504440 + #827470 + #D4C3BE + #000000 + #33302E + #F7EFED + #E7BDB0 + #FFDBCF + #2C160D + #E7BDB0 + #5D4036 + #F5DED6 + #251915 + #D8C2BB + #53433E + #ECE3BB + #201C04 + #CFC7A1 + #4C472A + #E0D8D6 + #FFF8F6 + #FFFFFF + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF + #593C32 + #FFFFFF + #87665A + #FFFFFF + #4F3F3B + #FFFFFF + #83716B + #FFFFFF + #484327 + #FFFFFF + #746E4E + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FFF8F6 + #1E1B1A + #FFF8F6 + #1E1B1A + #F1DFD9 + #4C403D + #695C58 + #867873 + #000000 + #33302E + #F7EFED + #E7BDB0 + #8F6D61 + #FFFFFF + #74554A + #FFFFFF + #83716B + #FFFFFF + #695853 + #FFFFFF + #7B7554 + #FFFFFF + #625C3E + #FFFFFF + #E0D8D6 + #FFF8F6 + #FFFFFF + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF + #341C14 + #FFFFFF + #593C32 + #FFFFFF + #2C1F1B + #FFFFFF + #4F3F3B + #FFFFFF + #272209 + #FFFFFF + #484327 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FFF8F6 + #1E1B1A + #FFF8F6 + #000000 + #F1DFD9 + #2B221F + #4C403D + #4C403D + #000000 + #33302E + #FFFFFF + #FFE7E0 + #593C32 + #FFFFFF + #40261D + #FFFFFF + #4F3F3B + #FFFFFF + #372A25 + #FFFFFF + #484327 + #FFFFFF + #312D12 + #FFFFFF + #E0D8D6 + #FFF8F6 + #FFFFFF + #FAF2F0 + #F4ECEA + #EFE6E4 + #E9E1DF + + #505151 + #FFFFFF + #757575 + #FFFFFF + #5F5E5E + #FFFFFF + #E6E3E2 + #494848 + #535052 + #FFFFFF + #787476 + #FFFFFF + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #FCF8F8 + #1C1B1B + #FCF8F8 + #1C1B1B + #E0E3E3 + #444748 + #747878 + #C4C7C7 + #000000 + #313030 + #F4F0EF + #C7C6C6 + #E3E2E2 + #1B1C1C + #C7C6C6 + #464747 + #E5E2E1 + #1B1C1C + #C8C6C5 + #474746 + #E7E1E3 + #1D1B1D + #CBC5C7 + #494648 + #DDD9D8 + #FCF8F8 + #FFFFFF + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 + #424343 + #FFFFFF + #757575 + #FFFFFF + #434343 + #FFFFFF + #757474 + #FFFFFF + #454244 + #FFFFFF + #787476 + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #FCF8F8 + #1C1B1B + #FCF8F8 + #1C1B1B + #E0E3E3 + #404344 + #5C6060 + #787B7C + #000000 + #313030 + #F4F0EF + #C7C6C6 + #747474 + #FFFFFF + #5C5C5C + #FFFFFF + #757474 + #FFFFFF + #5C5C5C + #FFFFFF + #777375 + #FFFFFF + #5E5B5D + #FFFFFF + #DDD9D8 + #FCF8F8 + #FFFFFF + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 + #212222 + #FFFFFF + #424343 + #FFFFFF + #222222 + #FFFFFF + #434343 + #FFFFFF + #242223 + #FFFFFF + #454244 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #FCF8F8 + #1C1B1B + #FCF8F8 + #000000 + #E0E3E3 + #212525 + #404344 + #404344 + #000000 + #313030 + #FFFFFF + #EDECEB + #424343 + #FFFFFF + #2C2D2D + #FFFFFF + #434343 + #FFFFFF + #2D2D2D + #FFFFFF + #454244 + #FFFFFF + #2E2C2E + #FFFFFF + #DDD9D8 + #FCF8F8 + #FFFFFF + #F7F3F2 + #F1EDEC + #EBE7E7 + #E5E2E1 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 140539c9b..cc1da0f4c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,44 +7,43 @@ 16dp 20dp 24dp - 32dp + 28dp - 48dp - 56dp - 128dp - 192dp - 256dp + 48dp + 56dp + 64dp + 76dp + 80dp - 8dp - 16dp - 24dp - - 48dp - 56dp - 64dp - 64dp - 72dp + 128dp + 192dp + 256dp 24dp 32dp + 40dp + 48dp - 56dp + 48dp + 48dp + + + 72dp + 52dp + @dimen/spacing_medium + 30dp + 48dp + + 16dp + 128dp 14sp 22sp 2sp - - 3dp - - 78dp - 64dp - @dimen/spacing_medium - 28dp - 48dp - - 6dp + 10dp + 24dp 88dp 128dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 3256b7dca..a1cd46ef0 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,12 +9,15 @@ %d %1$s (%2$s) %s - %s - %1$s/%2$s Vorbis Opus Microsoft WAVE + Ogg %s + + + org.oxycblt.auxio.image.CoverProvider \ No newline at end of file diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 49dbceebd..901803a0c 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -14,10 +14,11 @@ auxio_rescan auxio_observing auxio_music_dirs - auxio_cover_mode + auxio_cover_mode2 auxio_square_covers auxio_include_dirs auxio_exclude_non_music + auxio_music_locations2 auxio_separators auxio_auto_sort_names @@ -52,6 +53,8 @@ auxio_artist_sort auxio_genre_sort + auxio_library_revision + @string/set_theme_auto @string/set_theme_day @@ -72,14 +75,16 @@ @string/set_cover_mode_off - @string/set_cover_mode_media_store - @string/set_cover_mode_quality + @string/set_cover_mode_save_space + @string/set_cover_mode_balanced + @string/set_cover_mode_high_quality @integer/cover_mode_off - @integer/cover_mode_media_store - @integer/cover_mode_quality + @integer/cover_mode_save_space + @integer/cover_mode_balanced + @integer/cover_mode_high_quality @@ -173,6 +178,7 @@ 0xA11B 0xA11C - 0xA11D - 0xA11E + 0xA125 + 0xA11D + 0xA11E \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8bdd5fa2e..37e68cc51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ Monitoring music library Retry + Pick folders More @@ -171,13 +172,6 @@ Use Windows-compatible paths - - State saved - - State cleared - - State restored - About Version Source code @@ -195,6 +189,9 @@ Author Alexander Capehart + Feedback + Make an issue on GitHub + Send an email Donate Supporters @@ -218,6 +215,11 @@ Starts Auxio using the previously saved state. If no saved state is available, all songs will be shuffled. Playback will start immediately. \n\nWARNING: Be careful controlling this service, if you close it and then try to use it again, you will probably crash the app. + Your songs will show up here. + Your albums will show up here. + Your artists will show up here. + Your genres will show up here. + Your playlists will show up here. @@ -280,8 +282,9 @@ Images Album covers Off - Fast - High quality + Save space + Balanced + High quality Force square album covers Crop all album covers to a 1:1 aspect ratio @@ -309,31 +312,16 @@ Warning: Changing the pre-amp to a high positive value may result in peaking on some audio tracks. Library - Music folders - Manage where music should be loaded from - Folders - - Mode - - Exclude - Music will not be loaded from the folders you add. - - Include - Music will only be loaded from the folders you add. + Music folders + Manage where music should be loaded from + Folders + New folder Refresh music Reload the music library, using cached tags when possible Rescan music Clear the tag cache and fully reload the music library (slower, but more complete) - Persistence - Save playback state - Save the current playback state now - Clear playback state - Clear the previously saved playback state (if any) - Restore playback state - Restore the previously saved playback state (if any) - No music found Music loading failed @@ -342,14 +330,8 @@ Unable to export the playlist to this file No app found that can handle this task - No folders - This folder is not supported - - Unable to restore state - - Unable to clear state - - Unable to save state + No folders + This folder is not supported @@ -361,7 +343,6 @@ Change repeat mode Turn shuffle on or off Shuffle all songs - Create a new playlist Stop playback Remove this song @@ -369,7 +350,7 @@ Open the queue Move this tab Clear search query - Remove folder + Remove folder Auxio icon Album cover @@ -382,6 +363,7 @@ + Unknown album Unknown artist Unknown genre No date @@ -398,14 +380,20 @@ MPEG-1 audio MPEG-4 audio + + MPEG-4 containing %s + + Advanced Audio Coding (AAC) + + Apple Lossless Audio Codec (ALAC) Ogg audio Matroska audio - - Advanced Audio Coding (AAC) Free Lossless Audio Codec (FLAC) + + Unknown diff --git a/app/src/main/res/values/styles_android.xml b/app/src/main/res/values/styles_android.xml index 98812140d..e31f6660e 100644 --- a/app/src/main/res/values/styles_android.xml +++ b/app/src/main/res/values/styles_android.xml @@ -7,7 +7,7 @@ 56dp to 48dp. --> @@ -85,13 +85,13 @@ diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index f43098404..6cc3b58da 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -18,7 +18,7 @@ + + + + + + + + - - - + + + + + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 74061c215..3667d53f9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,486 +1,786 @@ - - \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml index 86a3a6b03..607f2c7f2 100644 --- a/app/src/main/res/xml/preferences_music.xml +++ b/app/src/main/res/xml/preferences_music.xml @@ -37,7 +37,7 @@ + app:summary="@string/set_locations_desc" + app:title="@string/set_locations" /> diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt deleted file mode 100644 index 9914dbe5f..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * CacheRepositoryTest.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music.cache - -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerifyAll -import io.mockk.coVerifySequence -import io.mockk.just -import io.mockk.mockk -import io.mockk.slot -import java.lang.IllegalStateException -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.info.Date - -class CacheRepositoryTest { - @Test - fun cache_read_noInvalidate() { - val dao = - mockk { - coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) - } - val cacheRepository = CacheRepositoryImpl(dao) - val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - assertFalse(cache.invalidated) - - val songA = RawSong(mediaStoreId = 0, dateAdded = 1, dateModified = 2) - assertTrue(cache.populate(songA)) - assertEquals(RAW_SONG_A, songA) - - assertFalse(cache.invalidated) - - val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) - assertTrue(cache.populate(songB)) - assertEquals(RAW_SONG_B, songB) - - assertFalse(cache.invalidated) - } - - @Test - fun cache_read_invalidate() { - val dao = - mockk { - coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B)) - } - val cacheRepository = CacheRepositoryImpl(dao) - val cache = requireNotNull(runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - assertFalse(cache.invalidated) - - val nullStart = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) - val nullEnd = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) - assertFalse(cache.populate(nullStart)) - assertEquals(nullStart, nullEnd) - - assertTrue(cache.invalidated) - - val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) - assertTrue(cache.populate(songB)) - assertEquals(RAW_SONG_B, songB) - - assertTrue(cache.invalidated) - } - - @Test - fun cache_read_crashes() { - val dao = mockk { coEvery { readSongs() } throws IllegalStateException() } - val cacheRepository = CacheRepositoryImpl(dao) - assertEquals(null, runBlocking { cacheRepository.readCache() }) - coVerifyAll { dao.readSongs() } - } - - @Test - fun cache_write() { - var currentlyStoredSongs = listOf() - val insertSongsArg = slot>() - val dao = - mockk { - coEvery { nukeSongs() } answers { currentlyStoredSongs = listOf() } - - coEvery { insertSongs(capture(insertSongsArg)) } answers - { - currentlyStoredSongs = insertSongsArg.captured - } - } - - val cacheRepository = CacheRepositoryImpl(dao) - - val rawSongs = listOf(RAW_SONG_A, RAW_SONG_B) - runBlocking { cacheRepository.writeCache(rawSongs) } - - val cachedSongs = listOf(CACHED_SONG_A, CACHED_SONG_B) - coVerifySequence { - dao.nukeSongs() - dao.insertSongs(cachedSongs) - } - assertEquals(cachedSongs, currentlyStoredSongs) - } - - @Test - fun cache_write_nukeCrashes() { - val dao = - mockk { - coEvery { nukeSongs() } throws IllegalStateException() - coEvery { insertSongs(listOf()) } just Runs - } - val cacheRepository = CacheRepositoryImpl(dao) - runBlocking { cacheRepository.writeCache(listOf()) } - coVerifyAll { dao.nukeSongs() } - } - - @Test - fun cache_write_insertCrashes() { - val dao = - mockk { - coEvery { nukeSongs() } just Runs - coEvery { insertSongs(listOf()) } throws IllegalStateException() - } - val cacheRepository = CacheRepositoryImpl(dao) - runBlocking { cacheRepository.writeCache(listOf()) } - coVerifySequence { - dao.nukeSongs() - dao.insertSongs(listOf()) - } - } - - private companion object { - val CACHED_SONG_A = - CachedSong( - mediaStoreId = 0, - dateAdded = 1, - dateModified = 2, - size = 3, - durationMs = 4, - replayGainTrackAdjustment = 5.5f, - replayGainAlbumAdjustment = 6.6f, - musicBrainzId = "Song MBID A", - name = "Song Name A", - sortName = "Song Sort Name A", - track = 7, - disc = 8, - subtitle = "Subtitle A", - date = Date.from("2020-10-10"), - albumMusicBrainzId = "Album MBID A", - albumName = "Album Name A", - albumSortName = "Album Sort Name A", - releaseTypes = listOf("Release Type A"), - artistMusicBrainzIds = listOf("Artist MBID A"), - artistNames = listOf("Artist Name A"), - artistSortNames = listOf("Artist Sort Name A"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), - albumArtistNames = listOf("Album Artist Name A"), - albumArtistSortNames = listOf("Album Artist Sort Name A"), - genreNames = listOf("Genre Name A"), - ) - - val RAW_SONG_A = - RawSong( - mediaStoreId = 0, - dateAdded = 1, - dateModified = 2, - size = 3, - durationMs = 4, - replayGainTrackAdjustment = 5.5f, - replayGainAlbumAdjustment = 6.6f, - musicBrainzId = "Song MBID A", - name = "Song Name A", - sortName = "Song Sort Name A", - track = 7, - disc = 8, - subtitle = "Subtitle A", - date = Date.from("2020-10-10"), - albumMusicBrainzId = "Album MBID A", - albumName = "Album Name A", - albumSortName = "Album Sort Name A", - releaseTypes = listOf("Release Type A"), - artistMusicBrainzIds = listOf("Artist MBID A"), - artistNames = listOf("Artist Name A"), - artistSortNames = listOf("Artist Sort Name A"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID A"), - albumArtistNames = listOf("Album Artist Name A"), - albumArtistSortNames = listOf("Album Artist Sort Name A"), - genreNames = listOf("Genre Name A"), - ) - - val CACHED_SONG_B = - CachedSong( - mediaStoreId = 9, - dateAdded = 10, - dateModified = 11, - size = 12, - durationMs = 13, - replayGainTrackAdjustment = 14.14f, - replayGainAlbumAdjustment = 15.15f, - musicBrainzId = "Song MBID B", - name = "Song Name B", - sortName = "Song Sort Name B", - track = 16, - disc = 17, - subtitle = "Subtitle B", - date = Date.from("2021-11-11"), - albumMusicBrainzId = "Album MBID B", - albumName = "Album Name B", - albumSortName = "Album Sort Name B", - releaseTypes = listOf("Release Type B"), - artistMusicBrainzIds = listOf("Artist MBID B"), - artistNames = listOf("Artist Name B"), - artistSortNames = listOf("Artist Sort Name B"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), - albumArtistNames = listOf("Album Artist Name B"), - albumArtistSortNames = listOf("Album Artist Sort Name B"), - genreNames = listOf("Genre Name B"), - ) - - val RAW_SONG_B = - RawSong( - mediaStoreId = 9, - dateAdded = 10, - dateModified = 11, - size = 12, - durationMs = 13, - replayGainTrackAdjustment = 14.14f, - replayGainAlbumAdjustment = 15.15f, - musicBrainzId = "Song MBID B", - name = "Song Name B", - sortName = "Song Sort Name B", - track = 16, - disc = 17, - subtitle = "Subtitle B", - date = Date.from("2021-11-11"), - albumMusicBrainzId = "Album MBID B", - albumName = "Album Name B", - albumSortName = "Album Sort Name B", - releaseTypes = listOf("Release Type B"), - artistMusicBrainzIds = listOf("Artist MBID B"), - artistNames = listOf("Artist Name B"), - artistSortNames = listOf("Artist Sort Name B"), - albumArtistMusicBrainzIds = listOf("Album Artist MBID B"), - albumArtistNames = listOf("Album Artist Name B"), - albumArtistSortNames = listOf("Album Artist Sort Name B"), - genreNames = listOf("Genre Name B"), - ) - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt deleted file mode 100644 index 9966c16e9..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * TextTagsTest.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music.metadata - -import androidx.media3.common.Metadata -import androidx.media3.extractor.metadata.flac.PictureFrame -import androidx.media3.extractor.metadata.id3.ApicFrame -import androidx.media3.extractor.metadata.id3.InternalFrame -import androidx.media3.extractor.metadata.id3.TextInformationFrame -import androidx.media3.extractor.metadata.vorbis.VorbisComment -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -class TextTagsTest { - @Test - fun textTags_vorbis() { - val textTags = TextTags(VORBIS_METADATA) - assertTrue(textTags.id3v2.isEmpty()) - assertEquals(listOf("Wheel"), textTags.vorbis["title"]) - assertEquals(listOf("Paraglow"), textTags.vorbis["album"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"]) - assertEquals(listOf("2022"), textTags.vorbis["date"]) - assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) - assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["APIC"]) - } - - @Test - fun textTags_id3v2() { - val textTags = TextTags(ID3V2_METADATA) - assertTrue(textTags.vorbis.isEmpty()) - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) - } - - @Test - fun textTags_mp4() { - val textTags = TextTags(MP4_METADATA) - assertTrue(textTags.vorbis.isEmpty()) - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) - } - - @Test - fun textTags_id3v2_vorbis_combined() { - val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA)) - assertEquals(listOf("Wheel"), textTags.vorbis["title"]) - assertEquals(listOf("Paraglow"), textTags.vorbis["album"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.vorbis["artist"]) - assertEquals(listOf("2022"), textTags.vorbis["date"]) - assertEquals(listOf("ep"), textTags.vorbis["releasetype"]) - assertEquals(listOf("+2 dB"), textTags.vorbis["replaygain_track_gain"]) - assertEquals(null, textTags.id3v2["metadata_block_picture"]) - - assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"]) - assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"]) - assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"]) - assertEquals(listOf("2022"), textTags.id3v2["TDRC"]) - assertEquals(null, textTags.id3v2["APIC"]) - assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"]) - assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"]) - } - - companion object { - private val VORBIS_METADATA = - Metadata( - VorbisComment("TITLE", "Wheel"), - VorbisComment("ALBUM", "Paraglow"), - VorbisComment("ARTIST", "Parannoul"), - VorbisComment("ARTIST", "Asian Glow"), - VorbisComment("DATE", "2022"), - VorbisComment("RELEASETYPE", "ep"), - VorbisComment("METADATA_BLOCK_PICTURE", ""), - VorbisComment("REPLAYGAIN_TRACK_GAIN", "+2 dB"), - PictureFrame(0, "", "", 0, 0, 0, 0, byteArrayOf())) - - private val ID3V2_METADATA = - Metadata( - TextInformationFrame("TIT2", null, listOf("Wheel")), - TextInformationFrame("TALB", null, listOf("Paraglow")), - TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")), - TextInformationFrame("TDRC", null, listOf("2022")), - TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")), - TextInformationFrame("TXXX", "replaygain_track_gain", listOf("+2 dB")), - ApicFrame("", "", 0, byteArrayOf())) - - // MP4 atoms are mapped to ID3v2 text information frames by ExoPlayer, but can - // duplicate frames and have ---- mapped to InternalFrame. - private val MP4_METADATA = - Metadata( - TextInformationFrame("TIT2", null, listOf("Wheel")), - TextInformationFrame("TALB", null, listOf("Paraglow")), - TextInformationFrame("TPE1", null, listOf("Parannoul")), - TextInformationFrame("TPE1", null, listOf("Asian Glow")), - TextInformationFrame("TDRC", null, listOf("2022")), - TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")), - InternalFrame("com.apple.iTunes", "replaygain_track_gain", "+2 dB"), - ApicFrame("", "", 0, byteArrayOf())) - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt deleted file mode 100644 index e89c8d241..000000000 --- a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DeviceLibraryTest.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.music.user - -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Test -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicType -import org.oxycblt.auxio.music.device.AlbumImpl -import org.oxycblt.auxio.music.device.ArtistImpl -import org.oxycblt.auxio.music.device.DeviceLibraryImpl -import org.oxycblt.auxio.music.device.GenreImpl -import org.oxycblt.auxio.music.device.SongImpl -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.Path - -class DeviceLibraryTest { - - @Test - fun deviceLibrary_withSongs() { - val songUidA = Music.UID.auxio(MusicType.SONGS) - val songUidB = Music.UID.auxio(MusicType.SONGS) - val songA = - mockk { - every { uid } returns songUidA - every { durationMs } returns 0 - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val songB = - mockk { - every { uid } returns songUidB - every { durationMs } returns 1 - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(songA, songB), listOf(), listOf(), listOf()) - verify { - songA.finalize() - songB.finalize() - } - val foundSongA = deviceLibrary.findSong(songUidA)!! - assertEquals(songUidA, foundSongA.uid) - assertEquals(0L, foundSongA.durationMs) - val foundSongB = deviceLibrary.findSong(songUidB)!! - assertEquals(songUidB, foundSongB.uid) - assertEquals(1L, foundSongB.durationMs) - } - - @Test - fun deviceLibrary_withAlbums() { - val albumUidA = Music.UID.auxio(MusicType.ALBUMS) - val albumUidB = Music.UID.auxio(MusicType.ALBUMS) - val albumA = - mockk { - every { uid } returns albumUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val albumB = - mockk { - every { uid } returns albumUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(albumA, albumB), listOf(), listOf()) - verify { - albumA.finalize() - albumB.finalize() - } - val foundAlbumA = deviceLibrary.findAlbum(albumUidA)!! - assertEquals(albumUidA, foundAlbumA.uid) - assertEquals(0L, foundAlbumA.durationMs) - val foundAlbumB = deviceLibrary.findAlbum(albumUidB)!! - assertEquals(albumUidB, foundAlbumB.uid) - assertEquals(1L, foundAlbumB.durationMs) - } - - @Test - fun deviceLibrary_withArtists() { - val artistUidA = Music.UID.auxio(MusicType.ARTISTS) - val artistUidB = Music.UID.auxio(MusicType.ARTISTS) - val artistA = - mockk { - every { uid } returns artistUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val artistB = - mockk { - every { uid } returns artistUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = - DeviceLibraryImpl(listOf(), listOf(), listOf(artistA, artistB), listOf()) - verify { - artistA.finalize() - artistB.finalize() - } - val foundArtistA = deviceLibrary.findArtist(artistUidA)!! - assertEquals(artistUidA, foundArtistA.uid) - assertEquals(0L, foundArtistA.durationMs) - val foundArtistB = deviceLibrary.findArtist(artistUidB)!! - assertEquals(artistUidB, foundArtistB.uid) - assertEquals(1L, foundArtistB.durationMs) - } - - @Test - fun deviceLibrary_withGenres() { - val genreUidA = Music.UID.auxio(MusicType.GENRES) - val genreUidB = Music.UID.auxio(MusicType.GENRES) - val genreA = - mockk { - every { uid } returns genreUidA - every { durationMs } returns 0 - every { finalize() } returns this - } - val genreB = - mockk { - every { uid } returns genreUidB - every { durationMs } returns 1 - every { finalize() } returns this - } - val deviceLibrary = DeviceLibraryImpl(listOf(), listOf(), listOf(), listOf(genreA, genreB)) - verify { - genreA.finalize() - genreB.finalize() - } - val foundGenreA = deviceLibrary.findGenre(genreUidA)!! - assertEquals(genreUidA, foundGenreA.uid) - assertEquals(0L, foundGenreA.durationMs) - val foundGenreB = deviceLibrary.findGenre(genreUidB)!! - assertEquals(genreUidB, foundGenreB.uid) - assertEquals(1L, foundGenreB.durationMs) - } - - @Test - fun deviceLibrary_equals() { - val songA = - mockk { - every { uid } returns Music.UID.auxio(MusicType.SONGS) - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val songB = - mockk { - every { uid } returns Music.UID.auxio(MusicType.SONGS) - every { path } returns Path(mockk(), Components.parseUnix("./")) - every { finalize() } returns this - } - val album = - mockk { - every { uid } returns mockk() - every { finalize() } returns this - } - - val deviceLibraryA = DeviceLibraryImpl(listOf(songA), listOf(album), listOf(), listOf()) - val deviceLibraryB = DeviceLibraryImpl(listOf(songA), listOf(), listOf(), listOf()) - val deviceLibraryC = DeviceLibraryImpl(listOf(songB), listOf(album), listOf(), listOf()) - assertEquals(deviceLibraryA, deviceLibraryB) - assertEquals(deviceLibraryA.hashCode(), deviceLibraryA.hashCode()) - assertNotEquals(deviceLibraryA, deviceLibraryC) - assertNotEquals(deviceLibraryA.hashCode(), deviceLibraryC.hashCode()) - } -} diff --git a/build.gradle b/build.gradle index a81768d25..7bc5b84a9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,24 +1,50 @@ buildscript { ext { - kotlin_version = '1.9.23' - navigation_version = "2.5.3" + agp_version = '8.7.3' + kotlin_version = '2.0.21' + kotlin_coroutines_version = '1.10.1' + navigation_version = "2.8.3" hilt_version = '2.51.1' + room_version = '2.6.1' + core_version = '1.15.0' + desugaring_version = '2.1.3' + + min_sdk = 24 + target_sdk = 35 + ndk_version = "27.2.12479018" + } dependencies { // Hilt isn't compatible with the new plugin syntax yet. classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0" } } plugins { - id "com.android.application" version '8.4.0' apply false + // Android studio doesn't understand this syntax + //noinspection GradlePluginVersion + id "com.android.application" version "$agp_version" apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false + //noinspection GradlePluginVersion + id 'com.android.library' version "$agp_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false - id "com.google.devtools.ksp" version '1.9.23-1.0.20' apply false - id "com.diffplug.spotless" version "6.20.0" apply false + id "com.google.devtools.ksp" version '2.0.21-1.0.25' apply false + // We use spotless in the root build.gradle to apply to all modules. + id "com.diffplug.spotless" version "6.25.0" apply true } -tasks.register('clean', Delete) { - delete rootProject.buildDir -} \ No newline at end of file +spotless { + kotlin { + target "*/src/**/*.kt" + ktfmt().dropboxStyle() + licenseHeaderFile("NOTICE") + } + + cpp { + target("*/src/**/cpp/*.cpp", "*/src/**/cpp/*.h", "*/src/**/cpp/*.hpp") + eclipseCdt().configFile("eclipse-cdt.xml") + licenseHeaderFile("NOTICE") + } +} diff --git a/eclipse-cdt.xml b/eclipse-cdt.xml new file mode 100644 index 000000000..4e6c20576 --- /dev/null +++ b/eclipse-cdt.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fastlane/metadata/android/ar-IQ/full_description.txt b/fastlane/metadata/android/ar-IQ/full_description.txt new file mode 100644 index 000000000..3a19d3c9b --- /dev/null +++ b/fastlane/metadata/android/ar-IQ/full_description.txt @@ -0,0 +1,10 @@ +هذا النص يصف تطبيق كمشغل موسيقى محلي يتميز بواجهة مستخدم سريعة وموثوقة بدون الميزات غير الضرورية الموجودة في مشغلات الموسيقى الأخرى. يعتمد على مكتبات تشغيل الوسائط الحديثة ويقدم جودة مكتبة واستماع أفضل مقارنة بالتطبيقات التي تستخدم وظائف أندرويد قديمة. يتضمن ميزات مثل: + +- تشغيل مستند على +- واجهة مستخدم حديثة +- إدارة مجلدات تدعم بطاقات SD +- دعم Android Auto +- تشغيل بلا فجوات + + +‌‍‎‏‪‫‬‭‮‮‮ diff --git a/fastlane/metadata/android/ar-IQ/short_description.txt b/fastlane/metadata/android/ar-IQ/short_description.txt new file mode 100644 index 000000000..3f1d0c14f --- /dev/null +++ b/fastlane/metadata/android/ar-IQ/short_description.txt @@ -0,0 +1 @@ +مشغل موسيقى بسيط وعقلاني diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar-SA/full_description.txt new file mode 100644 index 000000000..637993ae6 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/full_description.txt @@ -0,0 +1,7 @@ +هذا النص يصف تطبيق كمشغل موسيقى محلي يتميز بواجهة مستخدم سريعة وموثوقة بدون الميزات غير الضرورية الموجودة في مشغلات الموسيقى الأخرى. يعتمد على مكتبات تشغيل الوسائط الحديثة ويقدم جودة مكتبة واستماع أفضل مقارنة بالتطبيقات التي تستخدم وظائف أندرويد قديمة. يتضمن ميزات مثل: + +- تشغيل مستند على +- واجهة مستخدم حديثة +- إدارة مجلدات تدعم بطاقات +- تشغيل بلا فجوات +- diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt new file mode 100644 index 000000000..3f1d0c14f --- /dev/null +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -0,0 +1 @@ +مشغل موسيقى بسيط وعقلاني diff --git a/fastlane/metadata/android/az/short_description.txt b/fastlane/metadata/android/az/short_description.txt new file mode 100644 index 000000000..66ca50172 --- /dev/null +++ b/fastlane/metadata/android/az/short_description.txt @@ -0,0 +1 @@ +Sadə, səmərəli musiqi səsləndirici diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt new file mode 100644 index 000000000..b959b6bc8 --- /dev/null +++ b/fastlane/metadata/android/bg/full_description.txt @@ -0,0 +1,25 @@ +Auxio е локален музикален плейър с бърз, надежден UI/UX без много безполезни функции, присъстващи в други музикални плейъри. Изграден от модерни библиотеки за възпроизвеждане на мултимедия, Auxio има превъзходна поддръжка на библиотека и качество на слушане в сравнение с други приложения, които използват остаряла функционалност на Android. Накратко,Възпроизвежда музика. + +Характеристики + +- Възпроизвеждане на базата на Media3 ExoPlayer +- Snappy UI, извлечен от най-новите указания за Material Design +- Убеден UX, който дава приоритет на лекотата на използване пред крайните случаи +- Персонализирано поведение +- Поддръжка за номера на дискове, множество изпълнители, типове издания, +точни/оригинални дати, етикети за сортиране и др +- Усъвършенствана система за изпълнители, която обединява изпълнители и изпълнители на албуми +- Управление на папки с SD карта +- Надеждна функционалност за плейлисти +- Устойчивост на състоянието на възпроизвеждане +- Android автоматична поддръжка +- Автоматично възпроизвеждане без пропуски +- Пълна поддръжка на ReplayGain (на MP3, FLAC, OGG, OPUS и MP4 файлове) +- Поддръжка на външен еквалайзер (напр. Wavelet) +- От край до край +- Поддръжка на вградени корици +- Функция за търсене +- Автоматично пускане на слушалки +- Стилни джаджи, които автоматично се адаптират към техния размер +- Напълно частно и офлайн +- Без заоблени корици на албуми (по подразбиране) diff --git a/fastlane/metadata/android/bg/short_description.txt b/fastlane/metadata/android/bg/short_description.txt new file mode 100644 index 000000000..b53e6a67c --- /dev/null +++ b/fastlane/metadata/android/bg/short_description.txt @@ -0,0 +1 @@ +Прост, рационален музикален плейър diff --git a/fastlane/metadata/android/cs/full_description.txt b/fastlane/metadata/android/cs/full_description.txt index f47808edf..e1e6af29c 100644 --- a/fastlane/metadata/android/cs/full_description.txt +++ b/fastlane/metadata/android/cs/full_description.txt @@ -12,6 +12,7 @@ přesná/původní data, štítky pro řazení a další - Správa složek podporující SD karty - Spolehlivá funkce seznamů skladeb - Uchovávání stavu přehrávání +- Podpora Android Auto - Automatické přehrávání bez mezer - Plná podpora ReplayGain (u souborů MP3, FLAC, OGG, OPUS a MP4) - Podpora externích přehrávačů (např. Wavelet) @@ -21,4 +22,4 @@ přesná/původní data, štítky pro řazení a další - Automatické přehrávání při připojení sluchátek - Stylové widgety, které se automaticky adaptují své velikosti - Plně soukromý a offline -- Žádné zakulacené obaly alb (ve výchozím nastavení) +- Žádné zaoblené obaly alb (ve výchozím nastavení) diff --git a/fastlane/metadata/android/cy/short_description.txt b/fastlane/metadata/android/cy/short_description.txt new file mode 100644 index 000000000..f69dd656d --- /dev/null +++ b/fastlane/metadata/android/cy/short_description.txt @@ -0,0 +1 @@ +Chwaraewr cerddoriaeth syml a synhwyrol diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index fc52bfe41..62f87e1ad 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -13,6 +13,7 @@ Auxio ist ein lokaler Musik-Player mit einer schnellen, verlässlichen UI/UX, ab - verlässliche Wiedergabelisten-Verwaltung - verlässliches Speichern des Wiedergabezustands - automatische lückenlose Wiedergabe +- Unterstützung für Android Auto - Vollständiger ReplayGain-Support (für MP3-, FLAC-, OGG-, OPUS- und MP4-Dateien) - Externer Equalizerunterstützung (z.B. Wavelet) - Edge-to-Edge diff --git a/fastlane/metadata/android/el/short_description.txt b/fastlane/metadata/android/el/short_description.txt new file mode 100644 index 000000000..752cd28a5 --- /dev/null +++ b/fastlane/metadata/android/el/short_description.txt @@ -0,0 +1 @@ +Μία απλή, λογική συσκευή αναπαραγωγής μουσικής diff --git a/fastlane/metadata/android/en-US/changelogs/59.txt b/fastlane/metadata/android/en-US/changelogs/59.txt new file mode 100644 index 000000000..22e2eff4d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/59.txt @@ -0,0 +1,3 @@ +Auxio 4.0.0 completely overhauls the user experience, with a refreshed design based on the latest Material Design specs +and a brand new music loader with signifigant improvements to device and tag support. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v4.0.0. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index c4a9fe74c..2ac55755c 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -6,8 +6,7 @@ Auxio is a local music player with a fast, reliable UI/UX without the many usele - Snappy UI derived from the latest Material Design guidelines - Opinionated UX that prioritizes ease of use over edge cases - Customizable behavior -- Support for disc numbers, multiple artists, release types, -precise/original dates, sort tags, and more +- Support for disc numbers, multiple artists, release types, precise/original dates, sort tags, and more - Advanced artist system that unifies artists and album artists - SD Card-aware folder management - Reliable playlisting functionality @@ -22,4 +21,4 @@ precise/original dates, sort tags, and more - Headset autoplay - Stylish widgets that automatically adapt to their size - Completely private and offline -- No rounded album covers (by default) \ No newline at end of file +- No rounded album covers (if you want them) diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png index 8330d9666..52a9ff852 100644 Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index e8519e33f..cc059cc53 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png index 6067c95ae..6657a9e38 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot0.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png index f36816339..fc7667973 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png index 958fdbc4c..8b71fa259 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png index d12b24814..ac44384b7 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png index e9c15227e..2a5d413a3 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png index 1ed0f49ec..0dc12f9d7 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot6.png deleted file mode 100644 index cc9ef761e..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot6.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/shot7.png deleted file mode 100644 index c5b2a17ad..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/shot7.png and /dev/null differ diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt index 854a23c2f..1ffe22d7e 100644 --- a/fastlane/metadata/android/es-ES/full_description.txt +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -1,24 +1,25 @@ -Auxio es un reproductor de música local con una UI/UX rápida y confiable sin las muchas características inútiles presentes en otros reproductores de música. Construido a partir de bibliotecas de reproducción de medios modernas, Auxio tiene un soporte de biblioteca y una calidad de escucha superiores en comparación con otras aplicaciones que usan una funcionalidad de Android obsoleta. En resumen, Reproduce música. +Auxio es un reproductor de música local con una interfaz de usuario y una experiencia de usuario rápidas y fiables sin las numerosas funciones inútiles presentes en otros reproductores de música. Creado a partir de bibliotecas de reproducción de medios modernas, Auxio tiene un soporte de biblioteca y una calidad de escucha superiores en comparación con otras aplicaciones que utilizan funciones obsoletas de Android. En resumen, reproduce música. Características -- Reproducción basada en Media3 ExoPlayer -- Interfaz de usuario ágil derivada de las últimas pautas de diseño de materiales -- UX obstinado que prioriza la facilidad de uso sobre los casos extremos +- Reproducción basada en ExoPlayer media 3 +- Interfaz de usuario ágil basada en las últimas directrices de Material Design +- Experiencia de usuario que prioriza la facilidad de uso sobre los casos extremos - Comportamiento personalizable -- Soporte para números de disco, múltiples artistas, tipos de lanzamiento, -fechas precisas/originales, ordenar etiquetas y más -- Sistema de artista avanzado que unifica artistas y artistas de álbumes +- Soporte para números de disco, múltiples artistas, tipos de lanzamiento +fechas precisas/originales, etiquetas de clasificación, etc. +- Sistema avanzado de artistas que unifica artistas y artistas de álbumes - Gestión de carpetas compatible con tarjetas SD -- Funcionalidad de lista de reproducción confiable -- Reproducción automática sin pausas +- Funcionalidad fiable de listas de reproducción - Persistencia del estado de reproducción +- Compatibilidad con Android auto +- Reproducción automática sin pausas - Compatibilidad total con ReplayGain (en archivos MP3, FLAC, OGG, OPUS y MP4) -- Soporte de ecualizador externo (ej. Wavelet) +- Compatibilidad con ecualizadores externos (por ejemplo, Wavelet) - Borde a borde -- Soporte de cubiertas incrustadas -- Funcionalidad de búsqueda -- Reproducción automática de auriculares -- Widgets con estilo que se adaptan automáticamente a su tamaño -- Completamente privado y fuera de línea +- Compatibilidad con carátulas incrustadas +- Función de búsqueda +- Reproducción automática con auriculares +- Widgets elegantes que se adaptan automáticamente a su tamaño +- Completamente privado y sin conexión - Sin carátulas redondeadas (por defecto) diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt new file mode 100644 index 000000000..14417fef1 --- /dev/null +++ b/fastlane/metadata/android/et/full_description.txt @@ -0,0 +1,25 @@ +Auxio on kohalikus nutiseadmes töötav kiire ja usaldusväärse kasutajaliidesega muusikamängija. Rakendusest on eemaldatud teises sarnastes rakendustes leiduv kasutu lisafunktsionaalsus. Kuna kasutusel on tänapäevased muusikateegid, siis Auxiol on teiste sarnaste rakendustega (kes kasutavad Androidis leiduvat aegunud funktsionaalsust) võrreldes suurepärane muusikakogu tugi ning taasesituse kvaliteet. Lühidalt: Auxio lihtsalt esitab muusikat. + +Funktsionaalsus + +- taasesitus kasutab Media3 ExoPlayeri teeke +- viimastest Material Designi põhimõtetest lähtuv kiire kasutajaliides +- arendajate omal hinnangul põhinev kasutajaliides, mis eelistab kasutuse lihtsust harvaesinevate olukordade lahendamisele +- kohandatav käitumine +- plaadinumbrite, mitme esitaja, väljaande tüübi, +täpsete avaldamise kuupäevade sortimiseks mõeldud siltide ja palju muu sarnase tugi +- tavalisest tõhusam esitajate haldus, mis normaliseerib esitajad ning albumi esitajad +- kaustade haldus, mis saab hakkama SD-kaartidega +- usaldusväärse esitusloendi loomine +- taasesituse oleku meeldejätmine +- tugi Android Auto liidestusele +- automaatne taasesitus lugudevahelise vaikusteta +- taasesituse valjuse tundlikkuse tugi (MP3, FLAC, OGG, OPUS ja MP4 failide pihul) +- välise ekvalaiseri tugi (nt. Wavelet) +- äärest-ääreni visuaal +- lõimitud albumikaante tugi +- otsing +- automaatne taasesitus kõrvaklappidest +- stiilsed vidinad, mis automaatselt kohandavad oma suurust +- täiesti privaatne ja võrguühendust mittevajav +- plaadikaante ümarad nurgad puuduvad (vaikimisi) diff --git a/fastlane/metadata/android/et/short_description.txt b/fastlane/metadata/android/et/short_description.txt new file mode 100644 index 000000000..b48a64763 --- /dev/null +++ b/fastlane/metadata/android/et/short_description.txt @@ -0,0 +1 @@ +Lihtne ja ratsionaalne muusikamängija diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt new file mode 100644 index 000000000..a1b92b47a --- /dev/null +++ b/fastlane/metadata/android/fi/full_description.txt @@ -0,0 +1,23 @@ +Auxio on paikallisen musiikin soitin nopealla ja luotettavalla käyttöliittymällä ilman useita turhia ominaisuuksia kuten muissa soittimissa. Perustuen moderneihin mediantoistokirjastoihin, Auxiolla on parempi tuki kirjastoille ja äänenlaatu verrattuna muihin sovelluksiin, jotka käyttävät vanhentuneita android toimintoja. Lyhyesti, Se soittaa musiikkia. + +Ominaisuudet + +- Toisto perustuu Media3 ExoPlayeriin +- Tyylikäs käyttöliittymä johdettu viimeisimmistä Material Design ohjeista +- Omapäinen käyttökokemus joka asettaa helppokäyttöisyyden etusijalle reunatapauksien sijaan +- Mukautettava käyttäytyminen +- Tukee levyjen numeroita, moninkertaisia artisteja, julkaisutyyppejä, tarkkoja/alkuperäisiä päivämääriä, tagijärjestystä ynnä muuta +- Edistynyt esittäjäjärjestelmä, joka yhdistää artistit ja albumiartistit +- SD-korttitietoinen kansionhallinta +- Varmatoimiset soittolistatoiminnot +- Android auto-tuki +- Automaattinen katkoton toisto +- Täysi ReplayGain tuki (MP3, FLAC, OGG, OPUS ja MP4 tiedostoissa) +- Tuki ulkoiselle taajuuskorjaimelle (esim. Wavelet) +- Reunasta reunaan +- Tukee kappaleen sisältämiä kansikuvia +- Hakutoiminto +- Automaattinen toisto kuulokkeilla +- Tyylikkäitä vimpaimia, jotka mukautuvat automaattisesti kokoonsa +- Täysin yksityinen ja verkkoyhteydetön +- Pyöristämättömät albumin kansikuvat (oletusarvoisesti) diff --git a/fastlane/metadata/android/fil/full_description.txt b/fastlane/metadata/android/fil/full_description.txt new file mode 100644 index 000000000..155bbc5bc --- /dev/null +++ b/fastlane/metadata/android/fil/full_description.txt @@ -0,0 +1,25 @@ +Ang Auxio ay isang na lokal ng manunugtog at meron siya ng malakas, at maaasahan ng "UI" o "UX" ay walang dami ng mga walang-halaga sa mga iba pang ng mga manunugtog. Ito ay gumawa ng mga bago ng "media playback libraries", at ang Auxio naman ay meron ng superyor ng "library support" at ang mahusay na pakikinig ng mga musika mo at kaysa sa mga iba ng mga "manunugtog na apps" ng gamit ng mga obseleto ng mga tungkulin. Madaling salita, Tumugtog ito ng mga musika. + +Mga Tampok + +- Pag-playback batay sa Media3 ExoPlayer +- Snappy UI na nagmula sa pinakabagong mga alituntunin sa Material Design +- Opinionated UX na inuuna ang kadalian ng paggamit kaysa sa mga edge case +- Nako-customize na pag-uugali +- Suporta para sa mga numero ng disc, maramihang mga artist, mga uri ng release, +tumpak/orihinal na mga petsa, pag-uuri ng mga tag, at higit pa +- Advanced na sistema ng artist na pinag-iisa ang mga artist at album artist +- Pamamahala ng folder na may kamalayan sa SD Card +- Maaasahang pag-andar ng playlist +- Pagpapatuloy ng estado ng pag-playback +- Suporta sa Android auto +- Awtomatikong walang gap na pag-playback +- Buong suporta sa ReplayGain (Sa mga MP3, FLAC, OGG, OPUS, at MP4 na mga file) +- Panlabas na suporta sa equalizer (hal. Wavelet) +- Gilid-sa-gilid +- Naka-embed na sumasaklaw sa suporta +- Pag-andar ng paghahanap +- Autoplay ng headset +- Mga naka-istilong widget na awtomatikong umaangkop sa kanilang laki +- Ganap na pribado at offline +- Walang mga bilugan na cover ng album (bilang default). diff --git a/fastlane/metadata/android/fil/short_description.txt b/fastlane/metadata/android/fil/short_description.txt new file mode 100644 index 000000000..756b1420d --- /dev/null +++ b/fastlane/metadata/android/fil/short_description.txt @@ -0,0 +1 @@ +Napakasimple, rasyonal na manunugtog. diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index aeb864c64..a2e737b5a 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -12,6 +12,7 @@ les dates précises/originales, le classement par tags, and plus encore - Carte SD reconnue par le système de dossiers - Fonction de liste de lecture efficace - Statut de lecture persistant +- Support d'Android auto - Support complet de ReplayGain (pour les fichiers MP3, FLAC, OGG, OPUS, et MP4) - Support pour égaliseur externe (ex. Wavelet) - Navigation bord-à-bord diff --git a/fastlane/metadata/android/he/full_description.txt b/fastlane/metadata/android/he/full_description.txt index 1cadc6eb3..f36d22df6 100644 --- a/fastlane/metadata/android/he/full_description.txt +++ b/fastlane/metadata/android/he/full_description.txt @@ -12,6 +12,8 @@ - ניהול תיקיות מודע לכרטיסי SD - פונקציונליות פלייליסטים אמינה - התמדה במצב ההשמעה +- תמיכה ב-Android Auto +- השמעה ללא פערים אוטומטית - תמיכה מלאה ב-ReplayGain (בקבצי MP3, FLAC, OGG, OPUS, ו-MP4) - תמיכה באיקוולייזר חיצוני (למשל, Wavelet) - קצה לקצה @@ -20,4 +22,4 @@ - ניגון אוטומטי באוזניות - ווידג'טים אלגנטיים שמתאימים את עצמם לגודלם אוטומטית - פרטי לגמרי ולא מקוון -- ללא עטיפות אלבום מעוגלות (אלא אם את.ה מעוניינ.ת בהם. אז זה אפשרי.) +- ללא עטיפות אלבום מעוגלות (כברירת מחדל. ניתן לשנות את זה.) diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt index be3a02a95..73982b4f2 100644 --- a/fastlane/metadata/android/hi/full_description.txt +++ b/fastlane/metadata/android/hi/full_description.txt @@ -12,6 +12,7 @@ Auxio एक तेज़, विश्वसनीय UI/UX वाला एक - एसडी कार्ड-जागरूक फ़ोल्डर प्रबंधन - विश्वसनीय प्लेलिस्टिंग कार्यक्षमता - प्लेबैक अवस्था दृढ़ता +- एंड्रॉयड ऑटो समर्थन - स्वचालित गैपलेस प्लेबैक - पूर्ण रीप्लेगैन समर्थन (MP3, FLAC, OGG, OPUS और MP4 फ़ाइलों पर) - बाहरी तुल्यकारक समर्थन (उदा: वेवलेट) diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt index 6647c70f4..0d40db862 100644 --- a/fastlane/metadata/android/hr/full_description.txt +++ b/fastlane/metadata/android/hr/full_description.txt @@ -11,6 +11,8 @@ precizni/izvorni datumi, sortiranje oznaka i više - Napredni sustav izvođača koji ujedinjuje izvođače i izvođače albuma - Upravljanje mapama koje podržava SD karticu - Pouzdana funkcija popisa pjesama +- Automaska podrška za Android +- Automatska reprodukcija bez prekida - Postojanost stanja reprodukcije - Puna podrška za ReplayGain (na MP3, FLAC, OGG, OPUS i MP4 datotekama) - Podrška za vanjski ekvilizator (npr. Wavelet) diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt new file mode 100644 index 000000000..3cb5b16cd --- /dev/null +++ b/fastlane/metadata/android/hu/full_description.txt @@ -0,0 +1,25 @@ +Az Auxio egy helyi zenelejátszó gyors, megbízható felhasználói felülettel/UX-szel, anélkül, hogy a többi zenelejátszóban jelen lévő sok haszontalan funkciót tartalmazna. A modern médialejátszó könyvtárakra épülő Auxio kiváló könyvtártámogatással és zenehallgatási minőséggel rendelkezik, mint más, elavult Android-funkciókat használó alkalmazások. Röviden: Zenét játszik. + +Funkciók + +- Lejátszás Media3 ExoPlayer alapú +- A legfrissebb anyagtervezési irányelvekből származó, lendületes felhasználói felület +- Véleményes UX, amely előnyben részesíti a könnyű használatot a szélső esetekben +- Testreszabható viselkedés +- Lemezszámok, több előadó, kiadástípusok támogatása, +pontos/eredeti dátumok, címkék rendezése stb +- Fejlett előadói rendszer, amely egyesíti az előadókat és az album előadókat +- SD-kártya-tudatos mappakezelés +- Megbízható lejátszási lista funkció +- Lejátszási állapot fennmaradása +- Android automatikus támogatás +- Automatikus hézagmentes lejátszás +- Teljes ReplayGain támogatás (MP3, FLAC, OGG, OPUS és MP4 fájlokon) +- Külső hangszínszabályzó támogatás (pl. Wavelet) +- Éltől szélig +- Beágyazott borítók támogatása +- Keresés funkció +- Headset automatikus lejátszása +- Stílusos kütyük, amelyek automatikusan alkalmazkodnak a méretükhöz +- Teljesen privát és offline +- Nincsenek lekerekített albumborítók (alapértelmezés szerint) diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt index db4cabec8..e72f8defb 100644 --- a/fastlane/metadata/android/it/full_description.txt +++ b/fastlane/metadata/android/it/full_description.txt @@ -1,4 +1,4 @@ -Auxio è un lettore musicale locale con un'interfaccia ed espereinza utente veloce e affidabile, senza le numerose funzioni inutili presenti in altri lettori musicali. Basato su moderne librerie di riproduzione multimediale, Auxio ha un supporto di libreria e una qualità di ascolto superiore rispetto ad altre applicazioni che utilizzano funzionalità Android obsolete. In breve, riproduce musica. +Auxio è un lettore musicale locale veloce e con un'interfaccia immediata, senza le numerose funzioni inutili presenti in altri lettori musicali. Basato su moderne librerie di riproduzione multimediale, Auxio ha un supporto di libreria e una qualità di ascolto superiore rispetto ad altre applicazioni che utilizzano funzionalità Android obsolete. In breve, riproduce musica. Caratteristiche @@ -12,6 +12,8 @@ date precise/originali, tag di ordinamento e altro ancora - Gestione delle cartelle consapevole delle schede SD - Gestione affidabile delle playlist - Persistenza dello stato di riproduzione +- Riproduzione automatica senza silenzio +- Supporto Android Auto - Supporto completo di ReplayGain (su file MP3, FLAC, OGG, OPUS e MP4) - Supporto di equalizzatori esterni (es. Wavelet) - Edge-to-edge diff --git a/fastlane/metadata/android/ko/full_description.txt b/fastlane/metadata/android/ko/full_description.txt index 45f7cc6ca..c03243343 100644 --- a/fastlane/metadata/android/ko/full_description.txt +++ b/fastlane/metadata/android/ko/full_description.txt @@ -12,6 +12,7 @@ Auxio는 다른 음악 플레이어에 존재하는 쓸모없는 기능 없이, - SD 카드를 지원하는 폴더 관리 기능 - 안정적인 재생 목록 기능 - 이전 재생 상태 기억 +- Android Auto 지원 - 자동 갭리스 재생 지원 - ReplayGain 완벽 지원 (MP3, FLAC, OGG, OPUS, MP4) - 외부 이퀄라이저 지원 (Wavelet 등) diff --git a/fastlane/metadata/android/lt/full_description.txt b/fastlane/metadata/android/lt/full_description.txt index 043666dfc..0cb30af0c 100644 --- a/fastlane/metadata/android/lt/full_description.txt +++ b/fastlane/metadata/android/lt/full_description.txt @@ -1,23 +1,25 @@ -Auxio yra vietinis muzikos grotuvas su greita, patikima UI/UX be daugybės nenaudingų funkcijų, esančių kituose muzikos grotuvuose. Sukurta remiantis iš šiuolaikinių medijos grojimo bibliotekų, Auxio turi geresnį bibliotekos palaikymą ir klausymo kokybę, palyginti su kitomis programomis, kurios naudoja pasenusias Android funkcijas. Trumpai tariant, jame groja muziką. +„Auxio“ – tai vietinis muzikos leistuvė su greita, patikima naudotojo sąsaja ir potyris be daugybės nenaudingų funkcijų, esančių kituose muzikos leistuvėse. Sukurta remiantis iš šiuolaikinių medijos įrašo perklausos bibliotekų, „Auxio“ turi geresnį bibliotekos palaikymą ir klausymo kokybę, palyginti su kitomis programeles, kurios naudoja pasenusias „Android“ funkcijas. Trumpai tariant, jame leidžiama muziką. Funkcijos -- Media3 ExoPlayer pagrįstas grojimas -- Sparti UI, sukurta pagal naujausias Material Design gaires -- Nuomonę turintis UX, kuriame prioritetas teikiamas naudojimo paprastumui, o ne kraštutiniam atvejui +- „Media3 ExoPlayer“ pagrįstas įrašo perklausa +- Sparti naudotojo sąsaja, sukurta pagal naujausias „Material Design“ gaires +- Nuomonę turintis naudotojo potyris, kuriame prioritetas teikiamas naudojimo paprastumui, o ne kraštutiniam atvejui - Pasirinktas elgesys - Palaikomas diskų numerių, kelių atlikėjų, leidinių tipų palaikymas, -tikslias/originalias datas, rūšiavimo žymas ir dar daugiau +tikslias / originalias datas, rūšiavimo žymes ir dar daugiau - Išplėstinė atlikėjų sistema, kuri suvienija atlikėjus ir albumų atlikėjus - SD kortelių aplankų valdymas - Patikima grojaraščių sudarymo funkcija -- Grojimo būsenos išsaugojimas -- Visiškas ReplayGain palaikymas (MP3, MP4, FLAC, OGG, OPUS ir MP4 failus) -- Išorinio ekvalaizerio funkcija (pvz., Wavelet) +- Įrašo perklausos būsenos išsaugojimas +- „Android Auto“ palaikymas +- Automatinis įrašo perklausa be tarpų +- Visiškas „ReplayGain“ palaikymas (MP3, MP4, FLAC, OGG, OPUS ir MP4 failuose) +- Išorinio ekvalaizerio funkcija (pvz., „Wavelet“) - Krašto iki krašto - Įterptųjų viršelių palaikymas - Paieškos funkcija -- Automatinis ausinių grojimas +- Automatinis ausinių leidimas - Stilingi valdikliai, kurie automatiškai prisitaiko prie savo dydžio -- Visiškai privatus ir neprisijungęs +- Visiškai privati ir neprisijungę - Jokių suapvalintų albumų viršelių (pagal numatytuosius nustatymus) diff --git a/fastlane/metadata/android/lt/short_description.txt b/fastlane/metadata/android/lt/short_description.txt index aaa18cd6a..0a1cd0b79 100644 --- a/fastlane/metadata/android/lt/short_description.txt +++ b/fastlane/metadata/android/lt/short_description.txt @@ -1 +1 @@ -Paprastas, racionalus muzikos grotuvas +Paprastas, racionalus muzikos leistuvė diff --git a/fastlane/metadata/android/lv/short_description.txt b/fastlane/metadata/android/lv/short_description.txt new file mode 100644 index 000000000..99729a7f6 --- /dev/null +++ b/fastlane/metadata/android/lv/short_description.txt @@ -0,0 +1 @@ +Ērts un racionāls mūzikas atskaņotājs diff --git a/fastlane/metadata/android/ne/short_description.txt b/fastlane/metadata/android/ne/short_description.txt new file mode 100644 index 000000000..6bc2f1915 --- /dev/null +++ b/fastlane/metadata/android/ne/short_description.txt @@ -0,0 +1 @@ +एक सरल, विवेकशिल संगीत प्लेयर diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 000000000..895806109 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,24 @@ +Auxio is een lokale muziekspeler met een snelle, betrouwbare UI/UX zonder vele nutteloze functies in andere muziekspelers. Gebouwd op basis van moderne media-afspeelbibliotheken bevat Auxio superieure bibliotheekondersteuning en luisterkwaliteit vergeleken met andere apps die verouderde Android-functionaliteit gebruiken. Kortom, Het speelt muziek. + +Functies + +- Afspelen gebaseerd op Media3 ExoPlayer +- Snelle gebruikersinterface afgeleid van de nieuwste Material Design-richtlijnen +- Eigenzinnige UX die gebruiksgemak boven randgevallen stelt +- Aanpasbaar gedrag +- Ondersteuning voor disc nummers, meerdere artiesten, release types, +exacte/originele data, sorteertags en meer +- Geavanceerd artiestensysteem dat artiesten en albumartiesten verenigt +- Beheer van SD-kaartmappen +- Betrouwbare functionaliteit voor afspeellijsten +- Persistentie van afspeelstatus +- Automatisch gapless afspelen +- Volledige ondersteuning voor ReplayGain (op MP3-, FLAC-, OGG-, OPUS- en MP4-bestanden) +- Ondersteuning voor externe equalizer (bijv. Wavelet) +- Rand-tot-rand +- Ondersteuning voor ingesloten omslagen +- Zoek functionaliteit +- Automatisch afspelen voor koptelefoon +- Stijlvolle widgets die zich automatisch aanpassen aan hun grootte +- Volledig privé en offline +- Geen afgeronde albumhoezen (standaard) diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 000000000..7b12963ee --- /dev/null +++ b/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Een eenvoudige, rationele muziekspeler diff --git a/fastlane/metadata/android/pa/full_description.txt b/fastlane/metadata/android/pa/full_description.txt index 646c41be8..25d95cc5c 100644 --- a/fastlane/metadata/android/pa/full_description.txt +++ b/fastlane/metadata/android/pa/full_description.txt @@ -12,6 +12,7 @@ Auxio ਇੱਕ ਤੇਜ਼, ਭਰੋਸੇਮੰਦ UI/UX ਵਾਲਾ ਇੱ - ਭਰੋਸੇਯੋਗ ਪਲੇਅਲਿਸਟਿੰਗ ਕਾਰਜਕੁਸ਼ਲਤਾ - ਭਰੋਸੇਯੋਗ ਪਲੇਅਬੈਕ ਸਥਿਤੀ ਸਥਿਰਤਾ +- ਐਂਡਰੌਇਡ ਆਟੋ ਸਪੋਰਟ - ਆਟੋਮੈਟਿਕ ਗੈਪਲੈੱਸ ਪਲੇਅਬੈਕ - ਪੂਰਾ ਰੀਪਲੇਅ-ਗੇਨ ਸਮਰਥਨ (MP3, FLAC, OGG, OPUS, ਅਤੇ MP4 ਫਾਈਲਾਂ 'ਤੇ) - ਬਾਹਰੀ ਈਕੋਲਾਈਜ਼ਰ ਦਾ ਸਮਰਥਨ (ਉਦਾਹਰਨ. ਵੇਵਲੇਟ) diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl/full_description.txt new file mode 100644 index 000000000..d7a85fdbd --- /dev/null +++ b/fastlane/metadata/android/pl/full_description.txt @@ -0,0 +1,25 @@ +Auxio jest lokalnym odtwarzaczem muzyki z szybkim i niezawodnym interfejsem użytkownika, pozbawionym wielu zbędnych funkcji obecnych w innych odtwarzaczach. Dzięki nowoczesnym bibliotekom odtwarzania mediów, Auxio oferuje lepszą obsługę biblioteki i wyższą jakość dźwięku w porównaniu do aplikacji korzystających z przestarzałych funkcji Androida. W skrócie, odtwarza muzykę. + +Funkcje + +- Odtwarzanie oparte na Media3 ExoPlayer +- Szybki interfejs oparty na najnowszych wytycznych Material Design +- Przemyślane UX, które priorytyzuje prostotę użytkowania nad skrajnymi przypadkami +- Personalizowalne zachowanie +- Wsparcie dla numerów dysków, wielu artystów, rodzajów wydań, +precyzyjnych/oryginalnych dat wydania, tagów sortujących i więcej +- Zaawansowany system artistów, który łączy artystów i artystów z albumu +- Zarządzanie folderami z wsparciem dla kart SD +- Niezawodna funkcjonalność z playlistami +- Utrzymanie stanu odtwarzania +- Wsparcie dla Android auto +- Automatyczne bezproblemowe odtwarzanie +- Pełne wsparcie dla ReplayGain (dla plików MP3, FLAC, OGG, OPUS, i MP4) +- Wsparcie dla zewnętrznego korektora (np. Wavelet) +- Obsługa ekranu od krawędzi do krawędzi +- Obsługa wbudowanych okładek +- Funkcje wyszukiwania +- Automatyczne odtwarzanie z headsetem +- Stylowe widgety dostosowujące się do ich rozmiaru +- Całkowicie prywatny i działający offline +- Bez zakrąglonych okładek (domyślnie) diff --git a/fastlane/metadata/android/pl/short_description.txt b/fastlane/metadata/android/pl/short_description.txt new file mode 100644 index 000000000..8fa861c52 --- /dev/null +++ b/fastlane/metadata/android/pl/short_description.txt @@ -0,0 +1 @@ +Prosty i praktyczny odtwarzacz muzyki diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt index bff79436f..3c263cbcf 100644 --- a/fastlane/metadata/android/pt-BR/full_description.txt +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -1,21 +1,24 @@ -O Auxio é um reprodutor de música local com UI/UX rápido e confiável, sem os muitos recursos inúteis presentes em outros reprodutores de música. Construído a partir do Exoplayer, o Auxio tem uma experiência de audição muito melhor em comparação com outros aplicativos que usam a API MediaPlayer nativa. Resumindo, Toca música. +Auxio é um reprodutor de música local com uma interface/experiência de usuário rápida e confiável sem os tantos recursos inúteis presentes em outros reprodutores de música. Baseado em bibliotecas modernas de reprodução de mídia, Auxio possui um suporte à biblioteca e qualidade de reprodução superior comparado a outros aplicativos que usam recursos desatualizadas do Android. Em resumo, Auxio toca música. -Recursos +Recursos: -- Reprodução baseada em ExoPlayer -- Snappy UI derivada das diretrizes mais recentes do Material Design -- UX opinativo que prioriza a facilidade de uso -- Comportamento personalizável -- Suporte à numeração de músicas nos discos, múltiplos artistas, tipos de lançamento, datas precisas/originais, ordenar tags, e mais -- Sistema de artistas avançado que unifica artistas e seus álbuns -- Gerenciamento de pastas com reconhecimento de cartão SD -- Persistência confiável da lista de reprodução -- Suporte completo ao ReplayGain (MP3, MP4, FLAC, OGG e OPUS) -- Funcionalidade de equalizador externo (como o Wavelet) -- De ponta a ponta -- Suporte para capas embutidas -- Funcionalidade de pesquisa -- Reprodução automática em fones de ouvido -- Widgets elegantes que se adaptam automaticamente ao seu tamanho -- Completamente privado e offline -- Sem capas de álbuns arredondadas (a menos que você as queira. Daí pode.) +- Reprodução baseada no Media3 ExoPlayer. +- Interface de usuário rápida derivada das últimas diretrizes do Material Design. +- Experiência de usuário opinativa que prioriza a facilidade de uso ao invés de priorizar situações incomuns. +- Comportamento customizável. +- Suporte a números de disco, vários artistas, tipos de lançamento, datas precisas/originais, metadados organizados e mais. +- Sistema de artistas avançado que unifica artistas e artistas de álbum. +- Gerenciamento de pastas com suporte a cartão SD. +- Funcionalidade confiável de criação de playlists. +- Persistência do estado de reprodução. +- Suporte ao Android Auto. +- Reprodução automática sem interrupções. +- Suporte completo a ReplayGain (nos arquivos FLAC, MP3, MP4, OGG e OPUS). +- Suporte externo a equalizador (por exemplo, Wavelet). +- De ponta a ponta. +- Suporte a capas embutidas. +- Função de pesquisa. +- Reprodução automática em fones de ouvido. +- Widgets elegantes que se adaptam automaticamente ao tamanho. +- Completamente privado e off-line. +- Sem capas de álbuns arredondadas (por padrão). diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index 0220f53b7..c54e911c6 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -1,23 +1,25 @@ -Auxio — это локальный музыкальный плеер с быстрым и надежным UI/UX без многих бесполезных функций, имеющихся в других музыкальных плеерах. Создан на основе Exoplayer, Auxio имеет лучшую поддержку библиотеки и качество прослушивания по сравнению с другими приложениями, которые используют устаревшие функции Android. Вкратце,он воспроизводит музыку < /b>. +Auxio — это локальный музыкальный плеер с быстрым и надежным UI/UX без многих бесполезных функций, имеющихся в других музыкальных плеерах. Основываясь на современных библиотеках воспроизведения мультимедийных файлов, Auxio имеет лучшую поддержку библиотеки и лучшее качество прослушивания по сравнению с другими приложениями, которые используют устаревшие функции Android. Вкратце,он воспроизводит музыку. Особенности -- Воспроизведение на основе ExoPlayer +- Воспроизведение на основе Media3 ExoPlayer - Быстрый UI создан в соответствии с последними рекомендациями Material Design - Продуманный UX, который предпочитает простоту использования над крайностями - Настраиваемое поведение - Поддержка номеров дисков, нескольких исполнителей, типов выпусков, -точные / оригинальные даты, теги сортировки и т. д +точные/оригинальные даты, теги сортировки и т.д. - Расширенная система исполнителей, объединяющая исполнителей и исполнителей альбомов - Управление папками на SD-карте -- Надёжное сохранение состояния воспроизведения +- Надежная функция составления плейлистов +- Сохранение состояния воспроизведения +- Поддержка Android Auto - Автоматическое воспроизведение без пауз - Полная поддержка ReplayGain (в файлах MP3, FLAC, OGG, OPUS и MP4) - Поддержка внешнего эквалайзера (например, Wavelet) - Дизайн от края до края - Поддержка встроенных обложек -- Функциональный поиск +- Поиск - Автоматическое воспроизведение в наушниках - Адаптивные виджеты -- Полностью частный и офлайн -- Никаких закруглённых обложек альбомов (по умолчанию) +- Полностью приватный и офлайн +- Никаких закруглённых обложек альбомов (по умолчанию) diff --git a/fastlane/metadata/android/sl/full_description.txt b/fastlane/metadata/android/sl/full_description.txt index c8ee9819d..b4e6bdeef 100644 --- a/fastlane/metadata/android/sl/full_description.txt +++ b/fastlane/metadata/android/sl/full_description.txt @@ -11,6 +11,7 @@ Auxio je lokalni predvajalnik glasbe z hitrim in zanesljivim uporabniškim vmesn - Upravljanje map na SD kartici - Zanesljiva funkcionalnost ustvarjanja seznama predvajanja - Trajnost stanja predvajanja +- Samodejno predvajanje brez lukenj (praznega zvoka) - Popolna podpora za ReplayGain tehnologijo (za MP3, FLAC, OGG, OPUS in MP4 datoteke) - Podpora za zunanje izenačevalnike (npr. Wavelet) - Od roba do roba diff --git a/fastlane/metadata/android/sq/full_description.txt b/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 000000000..464cc4e84 --- /dev/null +++ b/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,24 @@ +Auxio është një lexues muzikor lokal me një UI/UX të shpejtë dhe të besueshëm, pa shumë veçori të panevojshme të pranishme në lexues të tjerë muzikorë. I ndërtuar mbi bibliotekat moderne të riprodhimit të mediave, Auxio ofron mbështetje superiore për bibliotekën dhe cilësi dëgjimi më të mirë krahasuar me aplikacionet e tjera që përdorin funksione të vjetruara të Android. Me pak fjalë, luan muzikë. + +Veçori + +- Riprodhim i bazuar në Media3 ExoPlayer +- UI i shpejtë, i ndërtuar sipas udhëzimeve më të fundit të Material Design +- UX i thjeshtë dhe funksional që përparëson lehtësinë e përdorimit mbi rastet e rralla +- Sjellje e personalizueshme +- Mbështetje për numrat e disqeve, artistë të shumtë, lloje publikimesh, data të sakta/origjinale, etiketa renditjeje, dhe më shumë +- Sistem i avancuar për artistët, që bashkon artistët dhe artistët e albumeve +-Menaxhim i dosjeve me njohje për SD Card +-Funksionalitet i besueshëm për krijimin e listave +- Ruajtje e gjendjes së riprodhimit +- Mbështetje për Android Auto +- Riprodhim automatik pa ndërprerje +- Mbështetje e plotë për ReplayGain (në skedarët MP3, FLAC, OGG, OPUS dhe MP4) +- Mbështetje për ekualizues të jashtëm (p.sh., Wavelet) +- Shtrirje nga një skaj në tjetrin +- Mbështetje për kopertina të integruara +- Funksionalitet kërkimi +- Riprodhim automatik me kufje +- Widget-e elegante që përshtaten automatikisht me madhësinë e tyre +- Plotësisht privat dhe offline +- Nuk përdor kopertina albumesh me kënde të rrumbullakosura (në parazgjedhje) diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 000000000..25b2c606b --- /dev/null +++ b/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Një muzikë-luajtës i thjeshtë dhe racional diff --git a/fastlane/metadata/android/sv/full_description.txt b/fastlane/metadata/android/sv/full_description.txt new file mode 100644 index 000000000..810c803d6 --- /dev/null +++ b/fastlane/metadata/android/sv/full_description.txt @@ -0,0 +1 @@ +Auxio är en lokal musikspelare med en snabb och pålitlig UI/UX utan de många oanvändbara egenskaperna hos andra musikspelare. diff --git a/fastlane/metadata/android/ta/full_description.txt b/fastlane/metadata/android/ta/full_description.txt new file mode 100644 index 000000000..59d2cfb13 --- /dev/null +++ b/fastlane/metadata/android/ta/full_description.txt @@ -0,0 +1,25 @@ +ஆக்சியோ ஒரு உள்ளக இசை வீரர், மற்ற இசை வீரர்களில் பல பயனற்ற நற்பொருத்தங்கள் இல்லாமல் வேகமான, நம்பகமான UI/UX உடன். நவீன மீடியா பிளேபேக் நூலகங்களால் கட்டப்பட்ட ஆக்சியோ, காலாவதியான ஆண்ட்ராய்டு செயல்பாட்டைப் பயன்படுத்தும் பிற பயன்பாடுகளுடன் ஒப்பிடும்போது சிறந்த நூலக உதவி மற்றும் கேட்கும் தரத்தைக் கொண்டுள்ளது. சுருக்கமாக, இது இசையை இயக்குகிறது. + + நற்பொருத்தங்கள் + + - மீடியா 3 எக்சோப்ளேயரை அடிப்படையாகக் கொண்ட பிளேபேக் + - அண்மைக் கால பொருள் வடிவமைப்பு வழிகாட்டுதல்களிலிருந்து பெறப்பட்ட ச்னாப்பி இடைமுகம் + - எட்ச் வழக்குகளில் பயன்பாட்டை எளிதாக்கும் கருத்தியல் யுஎக்ச் + - தனிப்பயனாக்கக்கூடிய நடத்தை + - வட்டு எண்களுக்கான உதவி, பல கலைஞர்கள், வெளியீட்டு வகைகள், + துல்லியமான/அசல் தேதிகள், வரிசைப்படுத்தப்பட்ட குறிச்சொற்கள் மற்றும் பல + - கலைஞர்கள் மற்றும் ஆல்பம் கலைஞர்களை ஒன்றிணைக்கும் மேம்பட்ட கலைஞர் அமைப்பு + - எச்டி கார்டு-விழிப்புணர்வு கோப்புறை மேலாண்மை + - நம்பகமான பிளேலிச்டிங் செயல்பாடு + - பிளேபேக் நிலை விடாமுயற்சி + - ஆண்ட்ராய்டு ஆட்டோ உதவி + - தானியங்கி இடைவெளி பின்னணி + . + - வெளிப்புற சமநிலை உதவி (எ.கா. அலைவரிசை) + -விளிம்பு-க்கு-விளிம்பு + - உட்பொதிக்கப்பட்ட உதவி உதவி + - செயல்பாட்டைத் தேடுங்கள் + - எட்செட் ஆட்டோபிளே + - ச்டைலான விட்செட்டுகள் அவற்றின் அளவிற்கு தானாகவே மாற்றியமைக்கின்றன + - முற்றிலும் தனிப்பட்ட மற்றும் இணைப்பில்லாத + - வட்டமான ஆல்பம் கவர்கள் இல்லை (இயல்பாக) diff --git a/fastlane/metadata/android/ta/short_description.txt b/fastlane/metadata/android/ta/short_description.txt new file mode 100644 index 000000000..a25ec04e8 --- /dev/null +++ b/fastlane/metadata/android/ta/short_description.txt @@ -0,0 +1 @@ +ஒரு எளிய, பகுத்தறிவு இசை வீரர் diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt index 86460ef08..9a442c520 100644 --- a/fastlane/metadata/android/tr/full_description.txt +++ b/fastlane/metadata/android/tr/full_description.txt @@ -12,6 +12,7 @@ kesin/orijinal tarihler, sıralama etiketleri ve daha fazlası - SD Card-aware klasör yönetimi - Güvenilir çalma listesi işlevi - Oynatma durumu kalıcılığı +- Android Auto desteği - Tam ReplayGain desteği (MP3, FLAC, OGG, OPUS ve MP4 dosyalarında) - Harici ekolayzer desteği (örn. Wavelet) - Kenardan kenara diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index b2bf2642f..4fbd4baca 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -1,24 +1,25 @@ -Auxio – це локальний музичний плеєр зі швидким і надійним UI/UX без багатьох непотрібних функцій, наявних в інших музичних плеєрах. Створений на основі сучасних медіа-бібліотек відтворення, Auxio має кращу підтримку бібліотеки та якість прослуховування порівняно з іншими застосунками, які використовують застарілі функції Android. Одним словом, він відтворює музику. +Auxio — це локальний музичний плеєр із швидким і надійним інтерфейсом користувача/UX без багатьох марних функцій, наявних в інших музичних плеєрах. Створений на основі сучасних медіа-бібліотек, Auxio має чудову підтримку бібліотек і якість прослуховування порівняно з іншими програмами, які використовують застарілі функції Android. Коротше кажучи, Він відтворює музику. -Особливості +Функції -- Відтворення на основі Media 3 ExoPlayer -- Швидкий UI створений на основі останніх рекомендацій Material Design -- Продуманий UX, який надає перевагу простоті використання над крайнощами -- Налаштовувана поведінка -- Підтримка номерів дисків, кількох виконавців, типів випусків, -точні/оригінальні дати, теги сортування тощо -- Розширена система виконавців, яка об’єднує виконавців і виконавців альбомів -- Керування теками з підтримкою SD-картки -- Надійне функція списків відтворення -- Збереження стану відтворення -- Автоматичне відтворення без пропусків -- Повна підтримка ReplayGain (у файлах MP3, FLAC, OGG, OPUS і MP4) -- Підтримка зовнішнього еквалайзера (наприклад, Wavelet) -- Дизайн від краю до краю -- Підтримка вбудованих обкладинок -- Функціональний пошук -- Автоматичне відтворення в навушниках -- Стильні віджети, які автоматично підлаштовуються під розмір -- Повністю приватний і офлайн -- Жодних заокруглених обкладинок альбомів (за замовчуванням) +— Відтворення на основі Media3 ExoPlayer +- Snappy UI, створений на основі останніх рекомендацій Material Design +- Переконливий UX, який надає перевагу простоті використання над крайніми випадками +- Настроювана поведінка +- Підтримка номерів дисків, кількох виконавців, типів випусків, +точні/оригінальні дати, теги сортування тощо +— Розширена система виконавців, яка об’єднує виконавців і виконавців альбомів +- Керування папками з підтримкою SD-карти +— Надійна функція списків відтворення +— Постійність стану відтворення +- Підтримка Android auto +- Автоматичне безперервне відтворення +- Повна підтримка ReplayGain (у файлах MP3, FLAC, OGG, OPUS і MP4) +- Підтримка зовнішнього еквалайзера (наприклад, Wavelet) +- Від краю до краю +— Підтримка вбудованих обкладинок +- Функція пошуку +- Автовідтворення гарнітури +- Стильні віджети, які автоматично адаптуються до їх розміру +- Повністю приватний і офлайн +- Немає округлених обкладинок альбомів (за замовчуванням) diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index 5eeab94cf..cc2ef74c1 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -11,7 +11,8 @@ Auxio 是一款本地音乐播放器,它拥有快速、可靠的 UI/UX,没 - 统一“艺术家”和“专辑艺术家”的高级“艺术家”系统 - 文件夹管理功能可以感知到 SD 卡 - 可靠的播放列表功能 -- 回放状态持久化 +- 播放状态持久化 +- 支持 Android auto - 自动无缝回放 - 完整的回放增益支持(包括 MP3、FLAC、OGG、OPUS 和 MP4 文件) - 支持外部均衡器(如 Wavelet 这样的应用) diff --git a/fastlane/metadata/android/zh-Hant/full_description.txt b/fastlane/metadata/android/zh-Hant/full_description.txt index 26596541a..efd745793 100644 --- a/fastlane/metadata/android/zh-Hant/full_description.txt +++ b/fastlane/metadata/android/zh-Hant/full_description.txt @@ -10,6 +10,7 @@ Auxio 是一款本機音樂播放器,擁有快速且可靠的 UI/UX,不含 - 進階藝術家系統,統一藝術家與專輯藝術家 - 支援 SD 卡的資料夾管理 - 可靠的播放列表功能 +- 支援 Android Auto - 播放狀態持久性 - 完整的 ReplayGain 支援(適用於 MP3、FLAC、OGG、OPUS 和 MP4 檔案) - 外部均衡器支援(例如 Wavelet) diff --git a/gradle.properties b/gradle.properties index 87899875b..f28e39824 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,6 +20,8 @@ android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.enableR8.fullMode=true -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true -android.nonFinalResIds=true \ No newline at end of file +android.nonFinalResIds=true +org.gradle.parallel=true +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 43a6e163d..6a93cb7a1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/media b/media index 34b33175c..4b3084e1b 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 34b33175c00183dc95cdcb8c735033b6785041e1 +Subproject commit 4b3084e1b63185eaeffa7cac9d7015040e0e2aa5 diff --git a/musikr/.Rhistory b/musikr/.Rhistory new file mode 100644 index 000000000..e69de29bb diff --git a/musikr/.gitignore b/musikr/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/musikr/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/musikr/README.md b/musikr/README.md new file mode 100644 index 000000000..7ce1cb1aa --- /dev/null +++ b/musikr/README.md @@ -0,0 +1,23 @@ +# musikr + +Musikr is a highly opinionated multithreaded music loader that enables Auxio's advanced music functionality. +It completely bypasses Android's MediaStore and uses the [storage access framework (SAF)](https://developer.android.com/guide/topics/providers/document-provider) +and [taglib](https://taglib.org/) to replicate it's functionality with less bugs and more flexibility, further +expanding it with an advanced music model that leverages the wide variety of tags available in modern extended +specs. + +Warning that the API surface is: +- Extremely unstable, as it's a very thin shim on top of a constantly optimzied and updated music loader +- Minimized to only what the rest of the app uses or builds on, so you will need to patch it to extend +certain components + +Feel free to use this library as long as you follow Auxio's GPLv3 license. Note that the license is viral, so +if you wind up using this in a proprietary project, the entire project must be GPLv3 too. + +If you want to generate some docs for the unstable API, you can run + +```bash +./gradlew musikr:dokkaGeneratePublicationHtml +``` + +In the project root and it should produce a webpage in `musikr/build/dokka/html` diff --git a/musikr/build.gradle b/musikr/build.gradle new file mode 100644 index 000000000..80007d3bf --- /dev/null +++ b/musikr/build.gradle @@ -0,0 +1,103 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id "com.google.devtools.ksp" + id "com.diffplug.spotless" + id "kotlin-parcelize" + id "org.jetbrains.dokka" +} + +android { + namespace 'org.oxycblt.musikr' + compileSdk target_sdk + ndkVersion "$ndk_version" + + defaultConfig { + minSdk min_sdk + targetSdk target_sdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + externalNativeBuild { + cmake { + cppFlags "" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.22.1" + } + } + + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += "-Xjvm-default=all" + } + + buildFeatures { + buildConfig true + } +} + +dependencies { + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" + + // AndroidX + implementation "androidx.core:core-ktx:$core_version" + + // Database + implementation "androidx.room:room-runtime:$room_version" + ksp "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + + // Build + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugaring_version" + + // Testing + testImplementation "junit:junit:4.13.2" + testImplementation "io.mockk:mockk:1.13.7" + testImplementation "org.robolectric:robolectric:4.11" + testImplementation 'androidx.test:core-ktx:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' +} + +task assembleTaglib(type: Exec) { + def jniDir = "$projectDir/src/main/cpp" + def libs = new File("$jniDir/taglib/pkg") + if (libs.exists()) { + commandLine "true" + return + } + commandLine "sh", "-c", "$jniDir/build_taglib.sh $jniDir $android.ndkDirectory" +} + +afterEvaluate { + preDebugBuild.dependsOn assembleTaglib + preReleaseBuild.dependsOn assembleTaglib +} + +clean { + delete "$projectDir/src/main/cpp/taglib/pkg" + delete "$projectDir/src/main/cpp/taglib/build" +} diff --git a/musikr/consumer-rules.pro b/musikr/consumer-rules.pro new file mode 100644 index 000000000..f15090dbc --- /dev/null +++ b/musikr/consumer-rules.pro @@ -0,0 +1,4 @@ +-keep class org.oxycblt.musikr.metadata.NativeInputStream { *; } +-keep class org.oxycblt.musikr.metadata.Metadata { *; } +-keep class org.oxycblt.musikr.metadata.Properties { *; } +-keep class org.oxycblt.musikr.metadata.NativeTagMap { *; } \ No newline at end of file diff --git a/musikr/proguard-rules.pro b/musikr/proguard-rules.pro new file mode 100644 index 000000000..8cc776ba8 --- /dev/null +++ b/musikr/proguard-rules.pro @@ -0,0 +1,26 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class org.oxycblt.musikr.metadata.NativeInputStream { *; } +-keep class org.oxycblt.musikr.metadata.Metadata { *; } +-keep class org.oxycblt.musikr.metadata.Properties { *; } +-keep class org.oxycblt.musikr.metadata.NativeTagMap { *; } \ No newline at end of file diff --git a/musikr/src/main/AndroidManifest.xml b/musikr/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5a898a4a2 --- /dev/null +++ b/musikr/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/musikr/src/main/cpp/CMakeLists.txt b/musikr/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..23bb28d13 --- /dev/null +++ b/musikr/src/main/cpp/CMakeLists.txt @@ -0,0 +1,71 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("tagJNI") # becomes "libtagJNI.so" + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +set(taglib_location "${CMAKE_CURRENT_SOURCE_DIR}/taglib") +set(taglib_pkg "${taglib_location}/pkg/${ANDROID_ABI}") +set(taglib_lib "${taglib_pkg}/lib") +set(taglib_include "${taglib_pkg}/include") + +set(taglib_file_name libtag.a) +set(taglib_file_path ${taglib_lib}/${taglib_file_name}) +set(taglib_lib_name, "taglib") +add_library( + "taglib" + STATIC + IMPORTED) +set_target_properties( + "taglib" PROPERTIES + IMPORTED_LOCATION + ${taglib_file_path} + INTERFACE_INCLUDE_DIRECTORIES + ${taglib_include}) +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + taglib_jni.cpp + JInputStream.cpp + JTagMap.cpp + JMetadataBuilder.cpp + JClassRef.cpp + JObjectRef.cpp + JStringRef.cpp + JByteArrayRef.cpp +) +target_link_options(${CMAKE_PROJECT_NAME} + # @Tolriq found that these flags can reduce the size of the linked + # taglib + jni shim shared library. Kudos to them. + # https://github.com/taglib/taglib/issues/1212#issuecomment-2326456903 + # Additionally, enable 16kb page size. I believe taglib can support this fine, + # as a cursory glance indicates that it doesn't hardcode any page sizes. + PRIVATE "-Wl,--exclude-libs,ALL,-z,max-page-size=16384") + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + PRIVATE android + PRIVATE log + PRIVATE taglib) diff --git a/musikr/src/main/cpp/JByteArrayRef.cpp b/musikr/src/main/cpp/JByteArrayRef.cpp new file mode 100644 index 000000000..297d9d859 --- /dev/null +++ b/musikr/src/main/cpp/JByteArrayRef.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Auxio Project + * JByteArrayRef.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JByteArrayRef.h" + +JByteArrayRef::JByteArrayRef(JNIEnv *env, TagLib::ByteVector &data) : env(env) { + auto size = static_cast(data.size()); + array = env->NewByteArray(size); + env->SetByteArrayRegion(array, 0, static_cast(size), + reinterpret_cast(data.data())); +} + +JByteArrayRef::JByteArrayRef(JNIEnv *env, jbyteArray array) : env(env), array( + array) { +} + +JByteArrayRef::~JByteArrayRef() { + env->DeleteLocalRef(array); +} + +TagLib::ByteVector JByteArrayRef::copy() { + jsize length = env->GetArrayLength(array); + auto data = env->GetByteArrayElements(array, nullptr); + TagLib::ByteVector byteVector(reinterpret_cast(data), length); + env->ReleaseByteArrayElements(array, data, JNI_ABORT); + return byteVector; +} + +jbyteArray& JByteArrayRef::operator*() { + return array; +} + diff --git a/musikr/src/main/cpp/JByteArrayRef.h b/musikr/src/main/cpp/JByteArrayRef.h new file mode 100644 index 000000000..02ce6e032 --- /dev/null +++ b/musikr/src/main/cpp/JByteArrayRef.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Auxio Project + * JByteArrayRef.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JBYTEARRAYREF_H +#define AUXIO_JBYTEARRAYREF_H + +#include +#include + +class JByteArrayRef { +public: + JByteArrayRef(JNIEnv *env, TagLib::ByteVector &data); + JByteArrayRef(JNIEnv *env, jbyteArray array); + + ~JByteArrayRef(); + + JByteArrayRef(const JByteArrayRef&) = delete; + + JByteArrayRef& operator=(const JByteArrayRef&) = delete; + + TagLib::ByteVector copy(); + + jbyteArray& operator*(); + +private: + JNIEnv *env; + jbyteArray array; +}; + +#endif //AUXIO_JBYTEARRAYREF_H diff --git a/musikr/src/main/cpp/JClassRef.cpp b/musikr/src/main/cpp/JClassRef.cpp new file mode 100644 index 000000000..8bf38a3b5 --- /dev/null +++ b/musikr/src/main/cpp/JClassRef.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Auxio Project + * JClassRef.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JClassRef.h" +JClassRef::JClassRef(JNIEnv *env, const char *classpath) : env(env) { + clazz = env->FindClass(classpath); +} + +JClassRef::~JClassRef() { + env->DeleteLocalRef(clazz); +} + +jmethodID JClassRef::method(const char *name, const char *signature) { + return env->GetMethodID(clazz, name, signature); +} + +jclass& JClassRef::operator*() { + return clazz; +} diff --git a/musikr/src/main/cpp/JClassRef.h b/musikr/src/main/cpp/JClassRef.h new file mode 100644 index 000000000..b9c5336b1 --- /dev/null +++ b/musikr/src/main/cpp/JClassRef.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Auxio Project + * JClassRef.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JCLASSREF_H +#define AUXIO_JCLASSREF_H + +#include + +class JClassRef { +public: + JClassRef(JNIEnv *env, const char *classpath); + + ~JClassRef(); + + JClassRef(const JClassRef&) = delete; + + JClassRef& operator=(const JClassRef&) = delete; + + // Only exists to work around a broken lint that doesn't + // realize that this class is a smart pointer to jclass. + jmethodID method(const char *name, const char *signature); + + jclass& operator*(); + +private: + JNIEnv *env; + jclass clazz; +}; + +#endif //AUXIO_JCLASSREF_H diff --git a/musikr/src/main/cpp/JInputStream.cpp b/musikr/src/main/cpp/JInputStream.cpp new file mode 100644 index 000000000..8c729a8db --- /dev/null +++ b/musikr/src/main/cpp/JInputStream.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Auxio Project + * JInputStream.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JInputStream.h" + +#include + +#include "JClassRef.h" +#include "JByteArrayRef.h" +#include "JStringRef.h" + +JInputStream::JInputStream(JNIEnv *env, jobject jInputStream) : env(env), jInputStream( + jInputStream) { + JClassRef jInputStreamClass = { env, + "org/oxycblt/musikr/metadata/NativeInputStream" }; + if (!env->IsInstanceOf(jInputStream, *jInputStreamClass)) { + throw std::runtime_error("Object is not NativeInputStream"); + } + jmethodID jInputStreamNameMethod = jInputStreamClass.method("name", + "()Ljava/lang/String;"); + jInputStreamReadBlockMethod = jInputStreamClass.method("readBlock", + "(Ljava/nio/ByteBuffer;)Z"); + jInputStreamIsOpenMethod = jInputStreamClass.method("isOpen", "()Z"); + jInputStreamSeekFromBeginningMethod = jInputStreamClass.method( + "seekFromBeginning", "(J)Z"); + jInputStreamSeekFromCurrentMethod = jInputStreamClass.method( + "seekFromCurrent", "(J)Z"); + jInputStreamSeekFromEndMethod = jInputStreamClass.method("seekFromEnd", + "(J)Z"); + jInputStreamTellMethod = jInputStreamClass.method("tell", "()J"); + jInputStreamLengthMethod = jInputStreamClass.method("length", "()J"); + JStringRef jName = { env, reinterpret_cast(env->CallObjectMethod( + jInputStream, jInputStreamNameMethod)) }; + _name = TagLib::String(env->GetStringUTFChars(*jName, nullptr)); +} + +JInputStream::~JInputStream() { + // The implicit assumption is that inputStream is managed by the owner, + // so we don't need to delete any references here +} + +TagLib::FileName /* const char * */JInputStream::name() const { + return _name.toCString(true); +} + +TagLib::ByteVector JInputStream::readBlock(size_t length) { + // We have to invert the buffer allocation here siits not a perfect system (vykeen instead of korvax0 but i warped all over the hub and i dont think its possible to find a "perfect" purple system like you would withnce the JVM ByteBuffer allocation system + // uses a bugged caching mechanism that leaks memory if used in multithreaded contexts. + TagLib::ByteVector buf { static_cast(length), 0 }; + jobject wrappedByteBuffer = env->NewDirectByteBuffer(buf.data(), + buf.size()); + if (wrappedByteBuffer == nullptr) { + throw std::runtime_error("Failed to wrap ByteBuffer"); + } + JObjectRef byteBuffer = { env, wrappedByteBuffer }; + jboolean result = env->CallBooleanMethod(jInputStream, + jInputStreamReadBlockMethod, *byteBuffer); + if (!result) { + throw std::runtime_error("Failed to read block, see logs"); + } + return buf; +} + +void JInputStream::writeBlock(const TagLib::ByteVector &data) { + throw std::runtime_error("Not implemented"); +} + +void JInputStream::insert(const TagLib::ByteVector &data, + TagLib::offset_t start, size_t replace) { + throw std::runtime_error("Not implemented"); +} + +void JInputStream::removeBlock(TagLib::offset_t start, size_t length) { + throw std::runtime_error("Not implemented"); +} + +bool JInputStream::readOnly() const { + return true; +} + +bool JInputStream::isOpen() const { + return env->CallBooleanMethod(jInputStream, jInputStreamIsOpenMethod); +} + +void JInputStream::seek(TagLib::offset_t offset, Position p) { + auto joffset = static_cast(std::llround(offset)); + jboolean result; + switch (p) { + case Beginning: + result = env->CallBooleanMethod(jInputStream, + jInputStreamSeekFromBeginningMethod, joffset); + break; + case Current: + result = env->CallBooleanMethod(jInputStream, + jInputStreamSeekFromCurrentMethod, joffset); + break; + case End: + result = env->CallBooleanMethod(jInputStream, + jInputStreamSeekFromEndMethod, joffset); + break; + } + if (!result) { + throw std::runtime_error("Failed to seek, see logs"); + } +} + +void JInputStream::clear() { + // Nothing to do +} + +TagLib::offset_t JInputStream::tell() const { + jlong jposition = env->CallLongMethod(jInputStream, jInputStreamTellMethod); + if (jposition == INT64_MIN) { + throw std::runtime_error("Failed to get position, see logs"); + } + return static_cast(jposition); +} + +TagLib::offset_t JInputStream::length() { + jlong jlength = env->CallLongMethod(jInputStream, jInputStreamLengthMethod); + if (jlength == INT64_MIN) { + throw std::runtime_error("Failed to get length, see logs"); + } + return static_cast(jlength); +} + +void JInputStream::truncate(TagLib::offset_t length) { + throw std::runtime_error("Not implemented"); +} + diff --git a/musikr/src/main/cpp/JInputStream.h b/musikr/src/main/cpp/JInputStream.h new file mode 100644 index 000000000..026e6c3c3 --- /dev/null +++ b/musikr/src/main/cpp/JInputStream.h @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 Auxio Project + * JInputStream.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JINPUTSTREAM_H +#define AUXIO_JINPUTSTREAM_H + +#include +#include "JObjectRef.h" + +#include "taglib/tiostream.h" +#include "taglib/tstring.h" + +class JInputStream: public TagLib::IOStream { +public: + JInputStream(JNIEnv *env, jobject jInputStream); + + ~JInputStream(); + + JInputStream(const JInputStream&) = delete; + JInputStream& operator=(const JInputStream&) = delete; + + /*! + * Returns the stream name in the local file system encoding. + */ + TagLib::FileName /* const char * */name() const override; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + TagLib::ByteVector readBlock(size_t length) override; + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns \c true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + void writeBlock(const TagLib::ByteVector &data) override; + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + void insert(const TagLib::ByteVector &data, TagLib::offset_t start = 0, + size_t replace = 0) override; + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + void removeBlock(TagLib::offset_t start = 0, size_t length = 0) override; + + /*! + * Returns \c true if the file is read only (or if the file can not be opened). + */ + bool readOnly() const override; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + bool isOpen() const override; + + /*! + * Move the I/O pointer to \a offset in the stream from position \a p. This + * defaults to seeking from the beginning of the stream. + * + * \see Position + */ + void seek(TagLib::offset_t offset, Position p = Beginning) override; + + /*! + * Reset the end-of-stream and error flags on the stream. + */ + void clear() override; + + /*! + * Returns the current offset within the stream. + */ + TagLib::offset_t tell() const override; + + /*! + * Returns the length of the stream. + */ + TagLib::offset_t length() override; + + /*! + * Truncates the stream to a \a length. + */ + void truncate(TagLib::offset_t length) override; + +private: + JNIEnv *env; + jobject jInputStream; + TagLib::String _name; + jmethodID jInputStreamReadBlockMethod; + jmethodID jInputStreamIsOpenMethod; + jmethodID jInputStreamSeekFromBeginningMethod; + jmethodID jInputStreamSeekFromCurrentMethod; + jmethodID jInputStreamSeekFromEndMethod; + jmethodID jInputStreamTellMethod; + jmethodID jInputStreamLengthMethod; +}; + +#endif //AUXIO_JINPUTSTREAM_H diff --git a/musikr/src/main/cpp/JMetadataBuilder.cpp b/musikr/src/main/cpp/JMetadataBuilder.cpp new file mode 100644 index 000000000..baab4df1a --- /dev/null +++ b/musikr/src/main/cpp/JMetadataBuilder.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 Auxio Project + * JMetadataBuilder.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JMetadataBuilder.h" + +#include "util.h" + +#include +#include +#include + +#include + +#include "JObjectRef.h" +#include "JClassRef.h" +#include "JStringRef.h" +#include "JByteArrayRef.h" + +JMetadataBuilder::JMetadataBuilder(JNIEnv *env) : env(env), id3v2(env), xiph( + env), mp4(env), cover(), properties(nullptr) { +} + +void JMetadataBuilder::setMimeType(TagLib::String type) { + mimeType = type; +} + +void JMetadataBuilder::setId3v1(TagLib::ID3v1::Tag &tag) { + id3v2.add_id("TIT2", tag.title()); + id3v2.add_id("TPE1", tag.artist()); + id3v2.add_id("TALB", tag.album()); + id3v2.add_id("TRCK", std::to_string(tag.track())); + id3v2.add_id("TYER", std::to_string(tag.year())); + const int genreNumber = tag.genreNumber(); + if (genreNumber != 255) { + id3v2.add_id("TCON", std::to_string(genreNumber)); + } +} + +void JMetadataBuilder::setId3v2(TagLib::ID3v2::Tag &tag) { + // We want to ideally find the front cover, fall back to the first picture otherwise. + std::optional firstPic; + std::optional frontCoverPic; + for (auto frame : tag.frameList()) { + if (auto txxxFrame = + dynamic_cast(frame)) { + TagLib::String id = frame->frameID(); + TagLib::StringList frameText = txxxFrame->fieldList(); + if (frameText.isEmpty()) + continue; + auto begin = frameText.begin(); + TagLib::String description = *begin; + frameText.erase(begin); + id3v2.add_combined(id, description, frameText); + } else if (auto textFrame = + dynamic_cast(frame)) { + TagLib::String key = frame->frameID(); + TagLib::StringList frameText = textFrame->fieldList(); + id3v2.add_id(key, frameText); + } else if (auto pictureFrame = + dynamic_cast(frame)) { + if (!firstPic) { + firstPic = pictureFrame; + } + if (!frontCoverPic + && pictureFrame->type() + == TagLib::ID3v2::AttachedPictureFrame::FrontCover) { + frontCoverPic = pictureFrame; + } + } else { + continue; + } + } + if (frontCoverPic) { + auto pic = *frontCoverPic; + cover = pic->picture(); + } else if (firstPic) { + auto pic = *firstPic; + cover = pic->picture(); + } +} + +void JMetadataBuilder::setXiph(TagLib::Ogg::XiphComment &tag) { + for (auto field : tag.fieldListMap()) { + auto key = field.first.upper(); + auto values = field.second; + xiph.add_custom(key, values); + } + auto pics = tag.pictureList(); + setFlacPictures(pics); +} + +template +void mp4AddImpl(JTagMap &map, TagLib::String &itemName, T itemValue) { + if (itemName.startsWith("----")) { + // Split this into it's atom name and description + auto split = itemName.find(':'); + auto atomName = itemName.substr(0, split); + auto atomDescription = itemName.substr(split + 1); + map.add_combined(atomName, atomDescription, itemValue); + } else { + map.add_id(itemName, itemValue); + } +} + +void JMetadataBuilder::setMp4(TagLib::MP4::Tag &tag) { + auto map = tag.itemMap(); + std::optional < TagLib::MP4::CoverArt > firstCover; + for (auto item : map) { + auto itemName = item.first; + auto itemValue = item.second; + if (itemName == "covr") { + // Special cover case. + // MP4 has no types, so just prioritize easier to decode covers (PNG, JPEG) + auto pics = itemValue.toCoverArtList(); + for (auto &pic : pics) { + auto format = pic.format(); + if (format == TagLib::MP4::CoverArt::PNG + || format == TagLib::MP4::CoverArt::JPEG) { + cover = pic.data(); + continue; + } + } + if (!pics.isEmpty()) { + cover = pics.front().data(); + } + continue; + } + auto type = itemValue.type(); + std::string serializedValue; + switch (type) { + // Normal expected MP4 items + case TagLib::MP4::Item::Type::StringList: + mp4AddImpl(mp4, itemName, itemValue.toStringList()); + break; + // Weird MP4 items I'm 90% sure I'll encounter. + case TagLib::MP4::Item::Type::Int: + serializedValue = std::to_string(itemValue.toInt()); + break; + case TagLib::MP4::Item::Type::UInt: + serializedValue = std::to_string(itemValue.toUInt()); + break; + case TagLib::MP4::Item::Type::LongLong: + serializedValue = std::to_string(itemValue.toLongLong()); + break; + case TagLib::MP4::Item::Type::IntPair: + // It's inefficient going from the integer representation back into + // a string, but I fully expect taggers to just write "NN/TT" strings + // anyway, and musikr doesn't have to do as much fiddly variant handling. + serializedValue = std::to_string(itemValue.toIntPair().first) + "/" + + std::to_string(itemValue.toIntPair().second); + break; + default: + // Don't care about the other types + continue; + } + mp4AddImpl(mp4, itemName, TagLib::String(serializedValue)); + } +} + +void JMetadataBuilder::setFlacPictures( + TagLib::List &pics) { + // Find the front cover image. If it doesn't exist, fall back to the first image. + for (auto pic : pics) { + if (pic->type() == TagLib::FLAC::Picture::FrontCover) { + cover = pic->data(); + return; + } + } + if (!pics.isEmpty()) { + cover = pics.front()->data(); + } +} + +void JMetadataBuilder::setProperties(TagLib::AudioProperties *properties) { + this->properties = properties; +} + +jobject JMetadataBuilder::build() { + JClassRef jPropertiesClass { env, "org/oxycblt/musikr/metadata/Properties" }; + jmethodID jPropertiesInitMethod = jPropertiesClass.method("", + "(Ljava/lang/String;JII)V"); + JStringRef jMimeType { env, this->mimeType }; + + JObjectRef jProperties { env, env->NewObject(*jPropertiesClass, + jPropertiesInitMethod, *jMimeType, + (jlong) properties->lengthInMilliseconds(), properties->bitrate(), + properties->sampleRate()) }; + + JClassRef jMetadataClass { env, "org/oxycblt/musikr/metadata/Metadata" }; + jmethodID jMetadataInitMethod = jMetadataClass.method("", + "(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/" + "oxycblt/musikr/metadata/Properties;)V"); + auto jId3v2Map = id3v2.getObject(); + auto jXiphMap = xiph.getObject(); + auto jMp4Map = mp4.getObject(); + if (cover.has_value()) { + JByteArrayRef jCoverArray { env, cover.value() }; + jobject result = env->NewObject(*jMetadataClass, jMetadataInitMethod, + **jId3v2Map, **jXiphMap, **jMp4Map, *jCoverArray, *jProperties); + return result; + } + return env->NewObject(*jMetadataClass, jMetadataInitMethod, **jId3v2Map, + **jXiphMap, **jMp4Map, nullptr, *jProperties); +} diff --git a/musikr/src/main/cpp/JMetadataBuilder.h b/musikr/src/main/cpp/JMetadataBuilder.h new file mode 100644 index 000000000..7bca6cbbb --- /dev/null +++ b/musikr/src/main/cpp/JMetadataBuilder.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Auxio Project + * JMetadataBuilder.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JMETADATABUILDER_H +#define AUXIO_JMETADATABUILDER_H + +#include +#include +#include + +#include "taglib/id3v1tag.h" +#include "taglib/id3v2tag.h" +#include "taglib/xiphcomment.h" +#include "taglib/mp4tag.h" +#include "taglib/audioproperties.h" + +#include "JTagMap.h" + +class JMetadataBuilder { +public: + JMetadataBuilder(JNIEnv *env); + + void setMimeType(TagLib::String type); + void setId3v1(TagLib::ID3v1::Tag &tag); + void setId3v2(TagLib::ID3v2::Tag &tag); + void setXiph(TagLib::Ogg::XiphComment &tag); + void setMp4(TagLib::MP4::Tag &tag); + void setFlacPictures(TagLib::List &pics); + void setProperties(TagLib::AudioProperties *properties); + + jobject build(); + +private: + JNIEnv *env; + + TagLib::String mimeType; + + std::optional cover; + TagLib::AudioProperties *properties; + + JTagMap id3v2; + JTagMap xiph; + JTagMap mp4; +}; + +#endif //AUXIO_JMETADATABUILDER_H diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/musikr/src/main/cpp/JObjectRef.cpp similarity index 62% rename from app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt rename to musikr/src/main/cpp/JObjectRef.cpp index 85e8e511e..9d6914cbf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt +++ b/musikr/src/main/cpp/JObjectRef.cpp @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023 Auxio Project - * DeviceModule.kt is part of Auxio. + * Copyright (c) 2025 Auxio Project + * JObjectRef.cpp is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,15 +16,15 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.device +#include "JObjectRef.h" -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -interface DeviceModule { - @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory +JObjectRef::JObjectRef(JNIEnv *env, jobject object) : env(env), object(object) { +} + +JObjectRef::~JObjectRef() { + env->DeleteLocalRef(object); +} + +jobject& JObjectRef::operator*() { + return object; } diff --git a/musikr/src/main/cpp/JObjectRef.h b/musikr/src/main/cpp/JObjectRef.h new file mode 100644 index 000000000..affc383e5 --- /dev/null +++ b/musikr/src/main/cpp/JObjectRef.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Auxio Project + * JObjectRef.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JOBJECTREF_H +#define AUXIO_JOBJECTREF_H + +#include +#include +#include +#include "JObjectRef.h" + +class JObjectRef { +public: + JObjectRef(JNIEnv *env, jobject object); + + ~JObjectRef(); + + JObjectRef(const JObjectRef&) = delete; + + JObjectRef& operator=(const JObjectRef&) = delete; + + jobject& operator*(); + +private: + JNIEnv *env; + jobject object; +}; + +#endif //AUXIO_JOBJECTREF_H diff --git a/musikr/src/main/cpp/JStringRef.cpp b/musikr/src/main/cpp/JStringRef.cpp new file mode 100644 index 000000000..c91b863d5 --- /dev/null +++ b/musikr/src/main/cpp/JStringRef.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Auxio Project + * JStringRef.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JStringRef.h" +#include "util.h" + +JStringRef::JStringRef(JNIEnv *env, jstring jString) : env(env), string(jString) { +} + +JStringRef::JStringRef(JNIEnv *env, const TagLib::String string) { + this->env = env; + this->string = env->NewStringUTF(string.toCString(true)); +} + +JStringRef::~JStringRef() { + env->DeleteLocalRef(string); +} + +TagLib::String JStringRef::copy() { + auto chars = env->GetStringUTFChars(string, nullptr); + TagLib::String result = chars; + env->ReleaseStringUTFChars(string, chars); + return result; +} + +jstring& JStringRef::operator*() { + return string; +} diff --git a/musikr/src/main/cpp/JStringRef.h b/musikr/src/main/cpp/JStringRef.h new file mode 100644 index 000000000..89fcb046d --- /dev/null +++ b/musikr/src/main/cpp/JStringRef.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Auxio Project + * JStringRef.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JSTRINGREF_H +#define AUXIO_JSTRINGREF_H + +#include +#include + +class JStringRef { +public: + JStringRef(JNIEnv *env, jstring jString); + + JStringRef(JNIEnv *env, TagLib::String string); + + ~JStringRef(); + + JStringRef(const JStringRef&) = delete; + + JStringRef& operator=(const JStringRef&) = delete; + + TagLib::String copy(); + + jstring& operator*(); + +private: + JNIEnv *env; + jstring string; +}; + +#endif //AUXIO_JSTRINGREF_H diff --git a/musikr/src/main/cpp/JTagMap.cpp b/musikr/src/main/cpp/JTagMap.cpp new file mode 100644 index 000000000..f229f3387 --- /dev/null +++ b/musikr/src/main/cpp/JTagMap.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 Auxio Project + * JTagMap.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "JTagMap.h" + +#include "JStringRef.h" + +JTagMap::JTagMap(JNIEnv *env) : env(env) { + auto jTagMapClass = std::make_unique < JClassRef + > (env, "org/oxycblt/musikr/metadata/NativeTagMap"); + auto jTagMapInitMethod = jTagMapClass->method("", "()V"); + jTagMap = std::move( + std::make_unique < JObjectRef + > (env, env->NewObject(**jTagMapClass, jTagMapInitMethod))); + jTagMapAddIdSingleMethod = jTagMapClass->method("addID", + "(Ljava/lang/String;Ljava/lang/String;)V"); + jTagMapAddIdListMethod = jTagMapClass->method("addID", + "(Ljava/lang/String;Ljava/util/List;)V"); + jTagMapAddCustomSingleMethod = jTagMapClass->method("addCustom", + "(Ljava/lang/String;Ljava/lang/String;)V"); + jTagMapAddCustomListMethod = jTagMapClass->method("addCustom", + "(Ljava/lang/String;Ljava/util/List;)V"); + jTagMapAddCombinedSingleMethod = jTagMapClass->method("addCombined", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + jTagMapAddCombinedListMethod = jTagMapClass->method("addCombined", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V"); + jTagMapGetObjectMethod = jTagMapClass->method("getObject", + "()Ljava/util/Map;"); + + jArrayListClass = std::make_unique < JClassRef + > (env, "java/util/ArrayList"); + jArrayListInitMethod = jArrayListClass->method("", "()V"); + jArrayListAddMethod = jArrayListClass->method("add", + "(Ljava/lang/Object;)Z"); +} + +void JTagMap::add_id(const TagLib::String id, const TagLib::String value) { + JStringRef jId { env, id }; + JStringRef jValue { env, value }; + env->CallVoidMethod(**jTagMap, jTagMapAddIdSingleMethod, *jId, *jValue); +} + +void JTagMap::add_id(const TagLib::String id, const TagLib::StringList values) { + JStringRef jId { env, id }; + JObjectRef jValues { env, env->NewObject(**jArrayListClass, + jArrayListInitMethod) }; + for (auto &value : values) { + JStringRef jValue { env, value }; + env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue); + } + env->CallVoidMethod(**jTagMap, jTagMapAddIdListMethod, *jId, *jValues); +} + +void JTagMap::add_custom(const TagLib::String description, + const TagLib::String value) { + JStringRef jDescription { env, description }; + JStringRef jValue { env, value }; + env->CallVoidMethod(**jTagMap, jTagMapAddCustomSingleMethod, *jDescription, + *jValue); +} + +void JTagMap::add_custom(const TagLib::String description, + const TagLib::StringList values) { + JStringRef jDescription { env, description }; + JObjectRef jValues { env, env->NewObject(**jArrayListClass, + jArrayListInitMethod) }; + for (auto &value : values) { + JStringRef jValue { env, value }; + env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue); + } + env->CallVoidMethod(**jTagMap, jTagMapAddCustomListMethod, *jDescription, + *jValues); +} + +void JTagMap::add_combined(const TagLib::String id, + const TagLib::String description, const TagLib::String value) { + JStringRef jId { env, id }; + JStringRef jDescription { env, description }; + JStringRef jValue { env, value }; + env->CallVoidMethod(**jTagMap, jTagMapAddCombinedSingleMethod, *jId, + *jDescription, *jValue); +} + +void JTagMap::add_combined(const TagLib::String id, + const TagLib::String description, const TagLib::StringList values) { + JStringRef jId { env, id }; + JStringRef jDescription { env, description }; + JObjectRef jValues { env, env->NewObject(**jArrayListClass, + jArrayListInitMethod) }; + for (auto &value : values) { + JStringRef jValue { env, value }; + env->CallBooleanMethod(*jValues, jArrayListAddMethod, *jValue); + } + env->CallVoidMethod(**jTagMap, jTagMapAddCombinedListMethod, *jId, + *jDescription, *jValues); +} + +std::unique_ptr JTagMap::getObject() { + return std::move( + std::make_unique < JObjectRef + > (env, env->CallObjectMethod(**jTagMap, + jTagMapGetObjectMethod))); +} diff --git a/musikr/src/main/cpp/JTagMap.h b/musikr/src/main/cpp/JTagMap.h new file mode 100644 index 000000000..95293db7b --- /dev/null +++ b/musikr/src/main/cpp/JTagMap.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Auxio Project + * JTagMap.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_JTAGMAP_H +#define AUXIO_JTAGMAP_H + +#include +#include +#include +#include + +#include "JObjectRef.h" +#include "JClassRef.h" + +class JTagMap { +public: + JTagMap(JNIEnv *env); + + JTagMap(const JTagMap&) = delete; + JTagMap& operator=(const JTagMap&) = delete; + + void add_id(TagLib::String id, TagLib::String value); + void add_id(TagLib::String id, TagLib::StringList values); + + void add_custom(TagLib::String description, TagLib::String value); + void add_custom(TagLib::String description, TagLib::StringList values); + + void add_combined(TagLib::String id, TagLib::String description, + TagLib::String value); + void add_combined(TagLib::String id, TagLib::String description, + TagLib::StringList values); + + std::unique_ptr getObject(); + +private: + JNIEnv *env; + + std::unique_ptr jTagMap; + jmethodID jTagMapAddIdSingleMethod; + jmethodID jTagMapAddIdListMethod; + jmethodID jTagMapAddCustomSingleMethod; + jmethodID jTagMapAddCustomListMethod; + jmethodID jTagMapAddCombinedSingleMethod; + jmethodID jTagMapAddCombinedListMethod; + jmethodID jTagMapGetObjectMethod; + + std::unique_ptr jArrayListClass; + jmethodID jArrayListInitMethod; + jmethodID jArrayListAddMethod; +}; + +#endif //AUXIO_JTAGMAP_H diff --git a/musikr/src/main/cpp/android.toolchain.cmake b/musikr/src/main/cpp/android.toolchain.cmake new file mode 100644 index 000000000..53123fef9 --- /dev/null +++ b/musikr/src/main/cpp/android.toolchain.cmake @@ -0,0 +1,15 @@ +# Define the minimum CMake version and project name +cmake_minimum_required(VERSION 3.22.1) + +# Set the Android NDK path +option(ANDROID_NDK_PATH "Path to Android NDK Install. Should be same version specified in gradle." REQUIRED) + +# Specify the target Android API level +set(ANDROID_PLATFORM android-24) + +# Define the toolchain +set(CMAKE_SYSTEM_NAME Android) +set(CMAKE_ANDROID_ARCH_ABI ${ANDROID_ABI}) +set(CMAKE_ANDROID_NDK ${ANDROID_NDK_PATH}) +set(CMAKE_ANDROID_STL_TYPE c++_static) +set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang) \ No newline at end of file diff --git a/musikr/src/main/cpp/build_taglib.sh b/musikr/src/main/cpp/build_taglib.sh new file mode 100755 index 000000000..3a4363d4b --- /dev/null +++ b/musikr/src/main/cpp/build_taglib.sh @@ -0,0 +1,43 @@ +set -e +WORKING_DIR=$1 +echo "Working directory is at $WORKING_DIR" +cd "$WORKING_DIR" + +TAGLIB_SRC_DIR=${WORKING_DIR}/taglib +TAGLIB_DST_DIR=${WORKING_DIR}/taglib/build +TAGLIB_PKG_DIR=${WORKING_DIR}/taglib/pkg +NDK_TOOLCHAIN=${WORKING_DIR}/android.toolchain.cmake +NDK_PATH=$2 +echo "Taglib source is at $TAGLIB_SRC_DIR" +echo "Taglib build is at $TAGLIB_DST_DIR" +echo "Taglib package is at $TAGLIB_PKG_DIR" +echo "NDK toolchain is at $NDK_TOOLCHAIN" +echo "NDK path is at $NDK_PATH" + +X86_ARCH=x86 +X86_64_ARCH=x86_64 +ARMV7_ARCH=armeabi-v7a +ARMV8_ARCH=arm64-v8a + +build_for_arch() { + local ARCH=$1 + local DST_DIR=$TAGLIB_DST_DIR/$ARCH + local PKG_DIR=$TAGLIB_PKG_DIR/$ARCH + + cd $TAGLIB_SRC_DIR + cmake -B $DST_DIR -DANDROID_NDK_PATH=${NDK_PATH} -DCMAKE_TOOLCHAIN_FILE=${NDK_TOOLCHAIN} \ + -DANDROID_ABI=$ARCH -DBUILD_SHARED_LIBS=OFF -DVISIBILITY_HIDDEN=ON -DBUILD_TESTING=OFF \ + -DBUILD_EXAMPLES=OFF -DBUILD_BINDINGS=OFF -DWITH_ZLIB=OFF -DCMAKE_BUILD_TYPE=Release \ + -DWITH_APE=OFF -DWITH_ASF=OFF -DWITH_ASF=OFF -DWITH_MOD=OFF -DWITH_SHORTEN=OFF \ + -DWITH_TRUEAUDIO=OFF -DCMAKE_CXX_FLAGS="-fPIC" + # Try to parallelize the build + cmake --build $DST_DIR --config Release -j$(nproc) + cd $WORKING_DIR + + cmake --install $DST_DIR --config Release --prefix $PKG_DIR --strip +} + +build_for_arch $X86_ARCH +build_for_arch $X86_64_ARCH +build_for_arch $ARMV7_ARCH +build_for_arch $ARMV8_ARCH diff --git a/musikr/src/main/cpp/taglib b/musikr/src/main/cpp/taglib new file mode 160000 index 000000000..ee1931b81 --- /dev/null +++ b/musikr/src/main/cpp/taglib @@ -0,0 +1 @@ +Subproject commit ee1931b81116cd0091c906896f6f4fb74850be51 diff --git a/musikr/src/main/cpp/taglib_jni.cpp b/musikr/src/main/cpp/taglib_jni.cpp new file mode 100644 index 000000000..abb94e9f7 --- /dev/null +++ b/musikr/src/main/cpp/taglib_jni.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2024 Auxio Project + * taglib_jni.cpp is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include "JInputStream.h" +#include "JMetadataBuilder.h" +#include "util.h" + +#include "taglib/fileref.h" +#include "taglib/flacfile.h" +#include "taglib/mp4file.h" +#include "taglib/mpegfile.h" +#include "taglib/opusfile.h" +#include "taglib/vorbisfile.h" +#include "taglib/wavfile.h" + +bool parseMpeg(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *mpegFile = dynamic_cast(file); + if (mpegFile == nullptr) { + return false; + } + auto id3v1Tag = mpegFile->ID3v1Tag(); + if (id3v1Tag != nullptr) { + try { + jBuilder.setId3v1(*id3v1Tag); + } catch (std::exception &e) { + LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what()); + } + } + auto id3v2Tag = mpegFile->ID3v2Tag(); + if (id3v2Tag != nullptr) { + try { + jBuilder.setId3v2(*id3v2Tag); + } catch (std::exception &e) { + LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what()); + } + } + return true; +} + +bool parseMp4(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *mp4File = dynamic_cast(file); + if (mp4File == nullptr) { + return false; + } + auto tag = mp4File->tag(); + if (tag != nullptr) { + try { + jBuilder.setMp4(*tag); + } catch (std::exception &e) { + LOGE("Unable to parse MP4 tag in %s: %s", name, e.what()); + } + } + return true; +} + +bool parseFlac(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *flacFile = dynamic_cast(file); + if (flacFile == nullptr) { + return false; + } + auto id3v1Tag = flacFile->ID3v1Tag(); + if (id3v1Tag != nullptr) { + try { + jBuilder.setId3v1(*id3v1Tag); + } catch (std::exception &e) { + LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what()); + } + } + auto id3v2Tag = flacFile->ID3v2Tag(); + if (id3v2Tag != nullptr) { + try { + jBuilder.setId3v2(*id3v2Tag); + } catch (std::exception &e) { + LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what()); + } + } + auto xiphComment = flacFile->xiphComment(); + if (xiphComment != nullptr) { + try { + jBuilder.setXiph(*xiphComment); + } catch (std::exception &e) { + LOGE("Unable to parse Xiph comment in %s: %s", name, e.what()); + } + } + auto pics = flacFile->pictureList(); + jBuilder.setFlacPictures(pics); + return true; +} + +bool parseOpus(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *opusFile = dynamic_cast(file); + if (opusFile == nullptr) { + return false; + } + auto tag = opusFile->tag(); + if (tag != nullptr) { + try { + jBuilder.setXiph(*tag); + } catch (std::exception &e) { + LOGE("Unable to parse Xiph comment in %s: %s", name, e.what()); + } + } + return true; +} + +bool parseVorbis(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *vorbisFile = dynamic_cast(file); + if (vorbisFile == nullptr) { + return false; + } + auto tag = vorbisFile->tag(); + if (tag != nullptr) { + try { + jBuilder.setXiph(*tag); + } catch (std::exception &e) { + LOGE("Unable to parse Xiph comment %s: %s", name, e.what()); + } + } + return true; +} + +bool parseWav(const char *name, TagLib::File *file, + JMetadataBuilder &jBuilder) { + auto *wavFile = dynamic_cast(file); + if (wavFile == nullptr) { + return false; + } + auto tag = wavFile->ID3v2Tag(); + if (tag != nullptr) { + try { + jBuilder.setId3v2(*tag); + } catch (std::exception &e) { + LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what()); + } + } + return true; +} + +extern "C" JNIEXPORT jobject JNICALL +Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env, + jobject /* this */, + jobject inputStream) { + const char *name = nullptr; + try { + JInputStream jStream {env, inputStream}; + name = jStream.name(); + TagLib::FileRef fileRef {&jStream}; + if (fileRef.isNull()) { + throw std::runtime_error("Invalid file"); + } + TagLib::File *file = fileRef.file(); + JMetadataBuilder jBuilder {env}; + jBuilder.setProperties(file->audioProperties()); + + // TODO: Make some type of composable logger so I don't + // have to shoehorn this into the native code. + if (parseMpeg(name, file, jBuilder)) { + jBuilder.setMimeType("audio/mpeg"); + } else if (parseMp4(name, file, jBuilder)) { + jBuilder.setMimeType("audio/mp4"); + } else if (parseFlac(name, file, jBuilder)) { + jBuilder.setMimeType("audio/flac"); + } else if (parseOpus(name, file, jBuilder)) { + jBuilder.setMimeType("audio/opus"); + } else if (parseVorbis(name, file, jBuilder)) { + jBuilder.setMimeType("audio/vorbis"); + } else if (parseWav(name, file, jBuilder)) { + jBuilder.setMimeType("audio/wav"); + } else { + LOGE("File format in %s is not supported", name); + return nullptr; + } + return jBuilder.build(); + } catch (std::exception &e) { + LOGE("Unable to parse metadata in %s: %s", name != nullptr ? name : "unknown file", e.what()); + return nullptr; + } +} diff --git a/musikr/src/main/cpp/util.h b/musikr/src/main/cpp/util.h new file mode 100644 index 000000000..ce9ad7255 --- /dev/null +++ b/musikr/src/main/cpp/util.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Auxio Project + * util.h is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef AUXIO_UTIL_H +#define AUXIO_UTIL_H + +#include +#include + +#define LOG_TAG "taglib_jni" +#define LOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#define LOGD(...) \ + ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)) + +#endif //AUXIO_UTIL_H diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt new file mode 100644 index 000000000..a793680db --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Auxio Project + * Config.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr + +import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.playlist.db.StoredPlaylists +import org.oxycblt.musikr.tag.interpret.Naming +import org.oxycblt.musikr.tag.interpret.Separators + +/** Side-effect laden [Storage] for use during music loading and [MutableLibrary] operation. */ +data class Storage( + /** + * A factory producing a repository of cached metadata to read and write from over the course of + * music loading. This will only be used during music loading. + */ + val cache: Cache.Factory, + + /** + * A repository of cover images to for re-use during music loading. Should be kept in lock-step + * with the cache for best performance. This will be used during music loading and when + * retrieving cover information from the library. + */ + val storedCovers: MutableCovers, + + /** + * A repository of user-created playlists that should also be loaded into the library. This will + * be used during music loading and mutated when creating, renaming, or deleting playlists in + * the library. + */ + val storedPlaylists: StoredPlaylists +) + +/** Configuration for how to interpret and extrapolate certain audio tags. */ +data class Interpretation( + /** How to construct names from audio tags. */ + val naming: Naming, + + /** What separators delimit multi-value audio tags. */ + val separators: Separators +) diff --git a/musikr/src/main/java/org/oxycblt/musikr/Library.kt b/musikr/src/main/java/org/oxycblt/musikr/Library.kt new file mode 100644 index 000000000..fea071da0 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/Library.kt @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024 Auxio Project + * Library.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr + +import org.oxycblt.musikr.fs.Path + +/** + * An immutable music library. + * + * No operations here will create side effects. + */ +interface Library { + val songs: Collection + val albums: Collection + val artists: Collection + val genres: Collection + val playlists: Collection + + /** + * Whether this library is empty (i.e no songs, which means no other music item) + * + * @return true if this library is empty, false otherwise + */ + fun empty(): Boolean + + /** + * Find a [Song] by it's [Music.UID] + * + * @param uid the [Music.UID] of the song + * @return the song if found, null otherwise + */ + fun findSong(uid: Music.UID): Song? + + /** + * Find a [Song] by it's [Path] + * + * @param path the [Path] of the song + * @return the song if found, null otherwise + */ + fun findSongByPath(path: Path): Song? + + /** + * Find an [Album] by it's [Music.UID] + * + * @param uid the [Music.UID] of the album + * @return the album if found, null otherwise + */ + fun findAlbum(uid: Music.UID): Album? + + /** + * Find an [Artist] by it's [Music.UID] + * + * @param uid the [Music.UID] of the artist + * @return the artist if found, null otherwise + */ + fun findArtist(uid: Music.UID): Artist? + + /** + * Find a [Genre] by it's [Music.UID] + * + * @param uid the [Music.UID] of the genre + * @return the genre if found, null otherwise + */ + fun findGenre(uid: Music.UID): Genre? + + /** + * Find a [Playlist] by it's [Music.UID] + * + * @param uid the [Music.UID] of the playlist + * @return the playlist if found, null otherwise + */ + fun findPlaylist(uid: Music.UID): Playlist? + + /** + * Find a [Playlist] by it's name + * + * @param name the name of the playlist + * @return the playlist if found, null otherwise + */ + fun findPlaylistByName(name: String): Playlist? +} + +/** + * A mutable extension of [Library]. + * + * Operations here will cause side-effects within the [Storage] used when this library was loaded. + * However, it won't actually mutate the [Library] itself, rather return a cloned instance with the + * changes applied. It is up to the client to update their reference to the library within their + * state handling. + */ +interface MutableLibrary : Library { + /** + * Create a new [Playlist] with the given name and songs. + * + * This will commit the new playlist to the stored playlists in the [Storage] used to load the + * library. + * + * @param name the name of the playlist + * @param songs the songs to add to the playlist + * @return a new [MutableLibrary] with the new playlist + */ + suspend fun createPlaylist(name: String, songs: List): MutableLibrary + + /** + * Rename a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to rename + * @param name the new name of the playlist + * @return a new [MutableLibrary] with the renamed playlist + */ + suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary + + /** + * Add songs to a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to add songs to + * @param songs the songs to add to the playlist + * @return a new [MutableLibrary] with the edited playlist + */ + suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary + + /** + * Remove songs from a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to remove songs from + * @param songs the songs to remove from the playlist + * @return a new [MutableLibrary] with the edited playlist + */ + suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary + + /** + * Remove a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to delete + * @return a new [MutableLibrary] with the edited playlist + */ + suspend fun deletePlaylist(playlist: Playlist): MutableLibrary +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/musikr/src/main/java/org/oxycblt/musikr/Music.kt similarity index 79% rename from app/src/main/java/org/oxycblt/auxio/music/Music.kt rename to musikr/src/main/java/org/oxycblt/musikr/Music.kt index 359afd9c3..5ae85f2d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Music.kt @@ -16,29 +16,25 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music +package org.oxycblt.musikr -import android.content.Context import android.net.Uri import android.os.Parcelable import androidx.room.TypeConverter import java.security.MessageDigest import java.util.UUID -import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.oxycblt.auxio.image.extractor.Cover -import org.oxycblt.auxio.image.extractor.ParentCover -import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.music.info.Disc -import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment -import org.oxycblt.auxio.util.concatLocalized -import org.oxycblt.auxio.util.toUuidOrNull +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.fs.Format +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.Disc +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.ReleaseType +import org.oxycblt.musikr.tag.ReplayGainAdjustment +import org.oxycblt.musikr.util.toUuidOrNull /** * Abstract music data. This contains universal information about all concrete music @@ -46,7 +42,7 @@ import org.oxycblt.auxio.util.toUuidOrNull * * @author Alexander Capehart (OxygenCobalt) */ -sealed interface Music : Item { +sealed interface Music { /** * A unique identifier for this music item. * @@ -81,23 +77,34 @@ sealed interface Music : Item { class UID private constructor( private val format: Format, - private val type: MusicType, + private val item: Item, private val uuid: UUID ) : Parcelable { // Cache the hashCode for HashMap efficiency. @IgnoredOnParcel private var hashCode = format.hashCode() init { - hashCode = 31 * hashCode + type.hashCode() + hashCode = 31 * hashCode + item.hashCode() hashCode = 31 * hashCode + uuid.hashCode() } override fun hashCode() = hashCode override fun equals(other: Any?) = - other is UID && format == other.format && type == other.type && uuid == other.uuid + other is UID && format == other.format && item == other.item && uuid == other.uuid - override fun toString() = "${format.namespace}:${type.intCode.toString(16)}-$uuid" + override fun toString() = "${format.namespace}:${item.intCode.toString(16)}-$uuid" + + internal enum class Item(val intCode: Int) { + // Item used to be MusicType back when the music module was + // part of Auxio, so these old integer codes remain. + // TODO: Introduce new UID format that removes these. + SONG(0xA10B), + ALBUM(0xA10A), + ARTIST(0xA109), + GENRE(0xA108), + PLAYLIST(0xA107) + } /** * Internal marker of [Music.UID] format type. @@ -117,7 +124,7 @@ sealed interface Music : Item { @TypeConverter fun fromMusicUID(uid: UID?) = uid?.toString() /** @see [Music.UID.fromString] */ - @TypeConverter fun toMusicUid(string: String?) = string?.let(UID::fromString) + @TypeConverter fun toMusicUid(string: String?) = string?.let(Companion::fromString) } companion object { @@ -125,23 +132,23 @@ sealed interface Music : Item { * Creates an Auxio-style [UID] of random composition. Used if there is no * non-subjective, unlikely-to-change metadata of the music. * - * @param type The analogous [MusicType] of the item that created this [UID]. + * @param item The type of [Item] that created this [UID]. */ - fun auxio(type: MusicType): UID { - return UID(Format.AUXIO, type, UUID.randomUUID()) + internal fun auxio(item: Item): UID { + return UID(Format.AUXIO, item, UUID.randomUUID()) } /** * Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective, * unlikely-to-change metadata of the music. * - * @param type The analogous [MusicType] of the item that created this [UID]. + * @param item The type of [Item] that created this [UID]. * @param updates Block to update the [MessageDigest] hash with the metadata of the * item. Make sure the metadata hashed semantically aligns with the format * specification. * @return A new auxio-style [UID]. */ - fun auxio(type: MusicType, updates: MessageDigest.() -> Unit): UID { + internal fun auxio(item: Item, updates: MessageDigest.() -> Unit): UID { val digest = MessageDigest.getInstance("SHA-256").run { updates() @@ -171,19 +178,19 @@ sealed interface Music : Item { .or(digest[13].toLong().and(0xFF).shl(16)) .or(digest[14].toLong().and(0xFF).shl(8)) .or(digest[15].toLong().and(0xFF))) - return UID(Format.AUXIO, type, uuid) + return UID(Format.AUXIO, item, uuid) } /** * Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID * extracted from a file. * - * @param type The analogous [MusicType] of the item that created this [UID]. + * @param item The [Item] that created this [UID]. * @param mbid The analogous MusicBrainz ID for this item that was extracted from a * file. * @return A new MusicBrainz-style [UID]. */ - fun musicBrainz(type: MusicType, mbid: UUID) = UID(Format.MUSICBRAINZ, type, mbid) + internal fun musicBrainz(item: Item, mbid: UUID) = UID(Format.MUSICBRAINZ, item, mbid) /** * Convert a [UID]'s string representation back into a concrete [UID] instance. @@ -211,8 +218,8 @@ sealed interface Music : Item { return null } - val type = - MusicType.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null + val intCode = ids[0].toIntOrNull(16) ?: return null + val type = Item.entries.firstOrNull { it.intCode == intCode } ?: return null val uuid = ids[1].toUuidOrNull() ?: return null return UID(format, type, uuid) } @@ -236,6 +243,7 @@ sealed interface MusicParent : Music { * @author Alexander Capehart (OxygenCobalt) */ interface Song : Music { + override val name: Name.Known /** The track number. Will be null if no valid track number was present in the metadata. */ val track: Int? /** The [Disc] number. Will be null if no valid disc number was present in the metadata. */ @@ -247,23 +255,32 @@ interface Song : Music { * audio file in a way that is scoped-storage-safe. */ val uri: Uri - /** Useful information to quickly obtain the album cover. */ - val cover: Cover /** * The [Path] to this audio file. This is only intended for display, [uri] should be favored * instead for accessing the audio file. */ val path: Path - /** The [MimeType] of the audio file. Only intended for display. */ - val mimeType: MimeType + /** The [Format] of the audio file. Only intended for display. */ + val format: Format /** The size of the audio file, in bytes. */ val size: Long /** The duration of the audio file, in milliseconds. */ val durationMs: Long + /** The bitrate of the audio file, in kbps. */ + val bitrateKbps: Int + /** The sample rate of the audio file, in Hz. */ + val sampleRateHz: Int /** The ReplayGain adjustment to apply during playback. */ val replayGainAdjustment: ReplayGainAdjustment - /** The date the audio file was added to the device, as a unix epoch timestamp. */ - val dateAdded: Long + /** + * The date last modified the audio file was last modified, in milliseconds since the unix + * epoch. + */ + val modifiedMs: Long + /** The time the audio file was added to the device, in milliseconds since the unix epoch. */ + val addedMs: Long + /** Useful information to quickly obtain the album cover. */ + val cover: Cover? /** * The parent [Album]. If the metadata did not specify an album, it's parent directory is used * instead. @@ -296,12 +313,12 @@ interface Album : MusicParent { * [ReleaseType.Album]. */ val releaseType: ReleaseType - /** Cover information from the template song used for the album. */ - val cover: ParentCover + /** Cover information from album's songs. */ + val covers: CoverCollection /** The duration of all songs in the album, in milliseconds. */ val durationMs: Long - /** The earliest date a song in this album was added, as a unix epoch timestamp. */ - val dateAdded: Long + /** The earliest date a song in this album was added, in milliseconds since the unix epoch. */ + val addedMs: Long /** * The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than * one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists @@ -327,7 +344,7 @@ interface Artist : MusicParent { */ val durationMs: Long? /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover + val covers: CoverCollection /** The [Genre]s of this artist. */ val genres: List } @@ -343,7 +360,7 @@ interface Genre : MusicParent { /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover + val covers: CoverCollection } /** @@ -357,34 +374,5 @@ interface Playlist : MusicParent { /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover? -} - -/** - * Run [Name.resolve] on each instance in the given list and concatenate them into a [String] in a - * localized manner. - * - * @param context [Context] required - * @return A concatenated string. - */ -fun List.resolveNames(context: Context) = - concatLocalized(context) { it.name.resolve(context) } - -/** - * Returns if [Music.name] matches for each item in a list. Useful for scenarios where the display - * information of an item must be compared without a context. - * - * @param other The list of items to compare to. - * @return True if they are the same (by [Music.name]), false otherwise. - */ -fun List.areNamesTheSame(other: List): Boolean { - for (i in 0 until max(size, other.size)) { - val a = getOrNull(i) ?: return false - val b = other.getOrNull(i) ?: return false - if (a.name != b.name) { - return false - } - } - - return true + val covers: CoverCollection } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt new file mode 100644 index 000000000..c18a01684 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024 Auxio Project + * Musikr.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr + +import android.content.Context +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import org.oxycblt.musikr.fs.MusicLocation +import org.oxycblt.musikr.pipeline.EvaluateStep +import org.oxycblt.musikr.pipeline.ExploreStep +import org.oxycblt.musikr.pipeline.ExtractStep + +/** + * A highly opinionated, multi-threaded device music library. + * + * Use this to load music with [run]. + * + * Note the following: + * 1. Musikr's API surface is intended to be primarily "stateless", with side-effects mostly + * contained within [Storage]. It's your job to manage long-term state. + * 2. There are no "defaults" in Musikr. You should think carefully about the parameters you are + * specifying and know consider they are desirable or not. + * 3. Musikr is currently not extendable, so if you're embedding this elsewhere you should be ready + * to fork and modify the source code. + */ +interface Musikr { + /** + * Start loading music from the given [locations] and the configuration provided earlier. + * + * @param locations The [MusicLocation]s to search for music in. + * @param onProgress Optional callback to receive progress on the current status of the music + * pipeline. Warning: These events will be rapid-fire. + * @return A handle to the newly created library alongside further cleanup. + */ + suspend fun run( + locations: List, + onProgress: suspend (IndexingProgress) -> Unit = {} + ): LibraryResult + + companion object { + /** + * Create a new instance from the given configuration. + * + * @param context The context to use for loading resources. + * @param storage Side-effect laden storage for use within the music loader **and** when + * mutating [MutableLibrary]. You should take responsibility for managing their long-term + * state. + * @param interpretation The configuration to use for interpreting certain vague tags. This + * should be configured by the user, if possible. + */ + fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr = + MusikrImpl( + storage, + ExploreStep.from(context, storage), + ExtractStep.from(context, storage), + EvaluateStep.new(storage, interpretation)) + } +} + +/** Simple library handle returned by [Musikr.run]. */ +interface LibraryResult { + val library: MutableLibrary + + /** + * Clean up expired resources. This should be done as soon as possible after music loading to + * reduce storage use. + * + * This may have unexpected results if previous [Library]s are in circulation across your app, + * so use it once you've fully updated your state. + */ + suspend fun cleanup() +} + +/** Music loading progress as reported by the music pipeline. */ +sealed interface IndexingProgress { + /** + * Currently indexing and extracting tags from device music. + * + * @param explored The amount of music currently found from the given [MusicLocation]s. + * @param loaded The amount of music that has had metadata extracted and parsed. + */ + data class Songs(val loaded: Int, val explored: Int) : IndexingProgress + + /** + * Currently creating the music graph alongside I/O finalization. + * + * There is no way to measure progress on these events. + */ + data object Indeterminate : IndexingProgress +} + +private class MusikrImpl( + private val storage: Storage, + private val exploreStep: ExploreStep, + private val extractStep: ExtractStep, + private val evaluateStep: EvaluateStep +) : Musikr { + override suspend fun run( + locations: List, + onProgress: suspend (IndexingProgress) -> Unit + ) = coroutineScope { + var exploredCount = 0 + var extractedCount = 0 + val explored = + exploreStep + .explore(locations) + .buffer(Channel.UNLIMITED) + .onStart { onProgress(IndexingProgress.Songs(0, 0)) } + .onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) } + val extracted = + extractStep + .extract(explored) + .buffer(Channel.UNLIMITED) + .onEach { onProgress(IndexingProgress.Songs(++extractedCount, exploredCount)) } + .onCompletion { onProgress(IndexingProgress.Indeterminate) } + val library = evaluateStep.evaluate(extracted) + LibraryResultImpl(storage, library) + } +} + +private class LibraryResultImpl( + private val storage: Storage, + override val library: MutableLibrary +) : LibraryResult { + override suspend fun cleanup() { + storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover }) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt new file mode 100644 index 000000000..277495d3a --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Auxio Project + * Cache.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cache + +import org.oxycblt.musikr.cover.Covers +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.pipeline.RawSong + +abstract class Cache { + internal abstract suspend fun read(file: DeviceFile, covers: Covers): CacheResult + + internal abstract suspend fun write(song: RawSong) + + internal abstract suspend fun finalize() + + abstract class Factory { + internal abstract fun open(): Cache + } +} + +internal sealed interface CacheResult { + data class Hit(val song: RawSong) : CacheResult + + data class Miss(val file: DeviceFile, val addedMs: Long?) : CacheResult +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt new file mode 100644 index 000000000..c4744c29e --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2023 Auxio Project + * CacheDatabase.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cache + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import org.oxycblt.musikr.cover.Covers +import org.oxycblt.musikr.cover.ObtainResult +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.metadata.Properties +import org.oxycblt.musikr.pipeline.RawSong +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.parse.ParsedTags +import org.oxycblt.musikr.util.correctWhitespace +import org.oxycblt.musikr.util.splitEscaped + +@Database(entities = [CachedSong::class], version = 58, exportSchema = false) +internal abstract class CacheDatabase : RoomDatabase() { + abstract fun visibleDao(): VisibleCacheDao + + abstract fun invisibleDao(): InvisibleCacheDao + + abstract fun writeDao(): CacheWriteDao + + companion object { + fun from(context: Context) = + Room.databaseBuilder( + context.applicationContext, CacheDatabase::class.java, "music_cache.db") + .fallbackToDestructiveMigration() + .build() + } +} + +@Dao +internal interface VisibleCacheDao { + @Query("SELECT * FROM CachedSong WHERE uri = :uri") + suspend fun selectSong(uri: String): CachedSong? + + @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri") + suspend fun selectAddedMs(uri: String): Long? + + @Transaction suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime()) + + @Query("UPDATE CachedSong SET touchedNs = :nowNs WHERE uri = :uri") + suspend fun updateTouchedNs(uri: String, nowNs: Long) +} + +@Dao +internal interface InvisibleCacheDao { + @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri") + suspend fun selectAddedMs(uri: String): Long? +} + +@Dao +internal interface CacheWriteDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong) + + @Query("DELETE FROM CachedSong WHERE touchedNs < :now") suspend fun pruneOlderThan(now: Long) +} + +@Entity +@TypeConverters(CachedSong.Converters::class) +internal data class CachedSong( + @PrimaryKey val uri: String, + val modifiedMs: Long, + val addedMs: Long, + val touchedNs: Long, + val mimeType: String, + val durationMs: Long, + val bitrateHz: Int, + val sampleRateHz: Int, + val musicBrainzId: String?, + val name: String, + val sortName: String?, + val track: Int?, + val disc: Int?, + val subtitle: String?, + val date: Date?, + val albumMusicBrainzId: String?, + val albumName: String?, + val albumSortName: String?, + val releaseTypes: List, + val artistMusicBrainzIds: List, + val artistNames: List, + val artistSortNames: List, + val albumArtistMusicBrainzIds: List, + val albumArtistNames: List, + val albumArtistSortNames: List, + val genreNames: List, + val replayGainTrackAdjustment: Float?, + val replayGainAlbumAdjustment: Float?, + val coverId: String?, +) { + suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? { + val cover = + when (val result = coverId?.let { covers.obtain(it) }) { + // We found the cover. + is ObtainResult.Hit -> result.cover + // We actually didn't find the cover, can't safely convert. + is ObtainResult.Miss -> return null + // No cover in the first place, can ignore. + null -> null + } + return RawSong( + file, + Properties(mimeType, durationMs, bitrateHz, sampleRateHz), + ParsedTags( + musicBrainzId = musicBrainzId, + name = name, + sortName = sortName, + durationMs = durationMs, + track = track, + disc = disc, + subtitle = subtitle, + date = date, + albumMusicBrainzId = albumMusicBrainzId, + albumName = albumName, + albumSortName = albumSortName, + releaseTypes = releaseTypes, + artistMusicBrainzIds = artistMusicBrainzIds, + artistNames = artistNames, + artistSortNames = artistSortNames, + albumArtistMusicBrainzIds = albumArtistMusicBrainzIds, + albumArtistNames = albumArtistNames, + albumArtistSortNames = albumArtistSortNames, + genreNames = genreNames, + replayGainTrackAdjustment = replayGainTrackAdjustment, + replayGainAlbumAdjustment = replayGainAlbumAdjustment), + cover = cover, + addedMs = addedMs) + } + + object Converters { + @TypeConverter + fun fromMultiValue(values: List) = + values.joinToString(";") { it.replace(";", "\\;") } + + @TypeConverter + fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace() + + @TypeConverter fun fromDate(date: Date?) = date?.toString() + + @TypeConverter fun toDate(string: String?) = string?.let(Date::from) + } + + companion object { + fun fromRawSong(rawSong: RawSong) = + CachedSong( + uri = rawSong.file.uri.toString(), + modifiedMs = rawSong.file.modifiedMs, + addedMs = rawSong.addedMs, + // Should be strictly monotonic so we don't prune this + // by accident later. + touchedNs = System.nanoTime(), + musicBrainzId = rawSong.tags.musicBrainzId, + name = rawSong.tags.name, + sortName = rawSong.tags.sortName, + durationMs = rawSong.tags.durationMs, + track = rawSong.tags.track, + disc = rawSong.tags.disc, + subtitle = rawSong.tags.subtitle, + date = rawSong.tags.date, + albumMusicBrainzId = rawSong.tags.albumMusicBrainzId, + albumName = rawSong.tags.albumName, + albumSortName = rawSong.tags.albumSortName, + releaseTypes = rawSong.tags.releaseTypes, + artistMusicBrainzIds = rawSong.tags.artistMusicBrainzIds, + artistNames = rawSong.tags.artistNames, + artistSortNames = rawSong.tags.artistSortNames, + albumArtistMusicBrainzIds = rawSong.tags.albumArtistMusicBrainzIds, + albumArtistNames = rawSong.tags.albumArtistNames, + albumArtistSortNames = rawSong.tags.albumArtistSortNames, + genreNames = rawSong.tags.genreNames, + replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment, + replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment, + coverId = rawSong.cover?.id, + mimeType = rawSong.properties.mimeType, + bitrateHz = rawSong.properties.bitrateKbps, + sampleRateHz = rawSong.properties.sampleRateHz) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt new file mode 100644 index 000000000..4707ffe3f --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Auxio Project + * StoredCache.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cache + +import android.content.Context +import org.oxycblt.musikr.cover.Covers +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.pipeline.RawSong + +interface StoredCache { + fun visible(): Cache.Factory + + fun invisible(): Cache.Factory + + companion object { + fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context)) + } +} + +private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : StoredCache { + override fun visible(): Cache.Factory = VisibleStoredCache.Factory(cacheDatabase) + + override fun invisible(): Cache.Factory = InvisibleStoredCache.Factory(cacheDatabase) +} + +private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() { + private val created = System.nanoTime() + + override suspend fun write(song: RawSong) = writeDao.updateSong(CachedSong.fromRawSong(song)) + + override suspend fun finalize() { + // Anything not create during this cache's use implies that it has not been + // access during this run and should be pruned. + writeDao.pruneOlderThan(created) + } +} + +private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) : + BaseStoredCache(writeDao) { + override suspend fun read(file: DeviceFile, covers: Covers): CacheResult { + val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null) + if (song.modifiedMs != file.modifiedMs) { + // We *found* this file earlier, but it's out of date. + // Send back it with the timestamp so it will be re-used. + // The touch timestamp will be updated on write. + return CacheResult.Miss(file, song.addedMs) + } + // Valid file, update the touch time. + visibleDao.touch(file.uri.toString()) + val rawSong = song.intoRawSong(file, covers) ?: return CacheResult.Miss(file, song.addedMs) + return CacheResult.Hit(rawSong) + } + + class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { + override fun open() = + VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao()) + } +} + +private class InvisibleStoredCache( + private val invisibleCacheDao: InvisibleCacheDao, + writeDao: CacheWriteDao +) : BaseStoredCache(writeDao) { + override suspend fun read(file: DeviceFile, covers: Covers) = + CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString())) + + class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { + override fun open() = + InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao()) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt new file mode 100644 index 000000000..52f9e15e3 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverFormat.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cover + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.OutputStream + +abstract class CoverFormat { + internal abstract val extension: String + + internal abstract fun transcodeInto(data: ByteArray, output: OutputStream): Boolean + + companion object { + fun jpeg(params: CoverParams): CoverFormat = + CoverFormatImpl("jpg", params, Bitmap.CompressFormat.JPEG) + } +} + +private class CoverFormatImpl( + override val extension: String, + private val params: CoverParams, + private val format: Bitmap.CompressFormat, +) : CoverFormat() { + override fun transcodeInto(data: ByteArray, output: OutputStream) = + BitmapFactory.Options().run { + inJustDecodeBounds = true + BitmapFactory.decodeByteArray(data, 0, data.size, this) + inSampleSize = calculateInSampleSize(params.resolution) + inJustDecodeBounds = false + val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, this) ?: return@run false + bitmap.compress(format, params.quality, output) + true + } + + private fun BitmapFactory.Options.calculateInSampleSize(size: Int): Int { + var inSampleSize = 1 + val (height, width) = outHeight to outWidth + + if (height > size || width > size) { + val halfHeight = height / 2 + val halfWidth = width / 2 + while ((halfHeight / inSampleSize) >= size && (halfWidth / inSampleSize) >= size) { + inSampleSize *= 2 + } + } + return inSampleSize + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt new file mode 100644 index 000000000..ef0917e05 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverIdentifier.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cover + +import java.security.MessageDigest + +interface CoverIdentifier { + suspend fun identify(data: ByteArray): String + + companion object { + fun md5(): CoverIdentifier = MD5CoverIdentifier() + } +} + +private class MD5CoverIdentifier() : CoverIdentifier { + @OptIn(ExperimentalStdlibApi::class) + override suspend fun identify(data: ByteArray): String { + val digest = + MessageDigest.getInstance("MD5").run { + update(data) + digest() + } + return digest.toHexString() + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt new file mode 100644 index 000000000..1b26dc63f --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverParams.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cover + +class CoverParams private constructor(val resolution: Int, val quality: Int) { + override fun hashCode() = 31 * resolution + quality + + override fun equals(other: Any?) = + other is CoverParams && other.resolution == resolution && other.quality == quality + + companion object { + fun of(resolution: Int, quality: Int): CoverParams { + check(resolution > 0) { "Resolution must be positive" } + check(quality in 0..100) { "Quality must be between 0 and 100" } + return CoverParams(resolution, quality) + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt new file mode 100644 index 000000000..e3f41b386 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Auxio Project + * Covers.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cover + +import java.io.InputStream + +interface Covers { + suspend fun obtain(id: String): ObtainResult +} + +interface MutableCovers : Covers { + suspend fun write(data: ByteArray): Cover + + suspend fun cleanup(excluding: Collection) +} + +sealed interface ObtainResult { + data class Hit(val cover: T) : ObtainResult + + class Miss : ObtainResult +} + +interface Cover { + val id: String + + suspend fun open(): InputStream? +} + +class CoverCollection private constructor(val covers: List) { + override fun hashCode() = covers.hashCode() + + override fun equals(other: Any?) = other is CoverCollection && covers == other.covers + + companion object { + fun from(covers: Collection) = + CoverCollection( + covers + .groupBy { it.id } + .entries + .sortedByDescending { it.key } + .sortedByDescending { it.value.size } + .map { it.value.first() }) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt new file mode 100644 index 000000000..4c8390869 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Auxio Project + * FileCovers.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.cover + +import android.os.ParcelFileDescriptor +import org.oxycblt.musikr.fs.app.AppFile +import org.oxycblt.musikr.fs.app.AppFiles + +open class FileCovers(private val appFiles: AppFiles, private val coverFormat: CoverFormat) : + Covers { + override suspend fun obtain(id: String): ObtainResult { + val file = appFiles.find(getFileName(id)) + return if (file != null) { + ObtainResult.Hit(FileCoverImpl(id, file)) + } else { + ObtainResult.Miss() + } + } + + protected fun getFileName(id: String) = "$id.${coverFormat.extension}" +} + +class MutableFileCovers( + private val appFiles: AppFiles, + private val coverFormat: CoverFormat, + private val coverIdentifier: CoverIdentifier +) : FileCovers(appFiles, coverFormat), MutableCovers { + override suspend fun write(data: ByteArray): FileCover { + val id = coverIdentifier.identify(data) + val file = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } + return FileCoverImpl(id, file) + } + + override suspend fun cleanup(excluding: Collection) { + val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) } + appFiles.deleteWhere { it !in used } + } +} + +interface FileCover : Cover { + suspend fun fd(): ParcelFileDescriptor? +} + +private data class FileCoverImpl(override val id: String, private val appFile: AppFile) : + FileCover { + override suspend fun fd() = appFile.fd() + + override suspend fun open() = appFile.open() +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt similarity index 69% rename from app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt index eec4918ea..6baac772f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023 Auxio Project - * DirectoryModule.kt is part of Auxio. + * Copyright (c) 2024 Auxio Project + * DeviceFile.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,10 +16,14 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.dirs +package org.oxycblt.musikr.fs -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent +import android.net.Uri -@Module @InstallIn(SingletonComponent::class) interface DirectoryModule {} +internal data class DeviceFile( + val uri: Uri, + val mimeType: String, + val path: Path, + val size: Long, + val modifiedMs: Long +) diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/Format.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/Format.kt new file mode 100644 index 000000000..113bd3583 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/Format.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 Auxio Project + * Format.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs + +import android.webkit.MimeTypeMap +import org.oxycblt.musikr.util.unlikelyToBeNull + +sealed interface Format { + val mimeType: String + + data object MPEG3 : Format { + override val mimeType = "audio/mpeg" + } + + data class MPEG4(val containing: Format?) : Format { + override val mimeType = "audio/mp4" + } + + data object AAC : Format { + override val mimeType = "audio/aac" + } + + data object ALAC : Format { + override val mimeType = "audio/alac" + } + + data class Ogg(val containing: Format?) : Format { + override val mimeType = "audio/ogg" + } + + data object Opus : Format { + override val mimeType = "audio/opus" + } + + data object Vorbis : Format { + override val mimeType = "audio/vorbis" + } + + data object FLAC : Format { + override val mimeType = "audio/flac" + } + + data object Wav : Format { + override val mimeType = "audio/wav" + } + + data class Unknown(override val mimeType: String) : Format { + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.uppercase() + } + + companion object { + private val CODEC_MAP = + mapOf( + "audio/mpeg" to MPEG3, + "audio/mp3" to MPEG3, + "audio/aac" to AAC, + "audio/aacp" to AAC, + "audio/3gpp" to AAC, + "audio/3gpp2" to AAC, + "audio/alac" to ALAC, + "audio/opus" to Opus, + "audio/vorbis" to Vorbis, + "audio/flac" to FLAC, + "audio/wav" to Wav, + "audio/raw" to Wav, + "audio/x-wav" to Wav, + "audio/vnd.wave" to Wav, + "audio/wave" to Wav, + ) + + internal fun infer(containerMimeType: String, codecMimeType: String): Format { + val codecFormat = CODEC_MAP[codecMimeType] + if (codecFormat != null) { + // Codec found, possibly wrap in container. + return unlikelyToBeNull(wrapInContainer(containerMimeType, codecFormat)) + } + val extensionFormat = CODEC_MAP[containerMimeType] + if (extensionFormat != null) { + // Standalone container of some codec. + return extensionFormat + } + return wrapInContainer(containerMimeType, null) ?: Unknown(containerMimeType) + } + + private fun wrapInContainer(containerMimeType: String, format: Format?) = + when (containerMimeType) { + "audio/mp4", + "audio/mp4a-latm", + "audio/mpeg4-generic" -> MPEG4(format) + "audio/ogg", + "application/ogg", + "application/x-ogg" -> Ogg(format) + else -> format + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt new file mode 100644 index 000000000..636d16c61 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Auxio Project + * MusicLocation.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.DocumentsContract +import org.oxycblt.musikr.fs.device.contentResolverSafe +import org.oxycblt.musikr.fs.path.DocumentPathFactory +import org.oxycblt.musikr.util.splitEscaped + +class MusicLocation private constructor(val uri: Uri, val path: Path) { + override fun equals(other: Any?) = other is MusicLocation && uri == other.uri + + override fun hashCode() = 31 * uri.hashCode() + + override fun toString(): String = uri.toString() + + companion object { + fun new(context: Context, uri: Uri): MusicLocation? { + if (!DocumentsContract.isTreeUri(uri)) return null + val documentPathFactory = DocumentPathFactory.from(context) + val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null + val notPersisted = + context.contentResolverSafe.persistedUriPermissions.none { + it.uri == uri && it.isReadPermission && it.isWritePermission + } + if (notPersisted) { + context.contentResolverSafe.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + return MusicLocation(uri, path) + } + + fun existing(context: Context, uri: Uri): MusicLocation? { + val documentPathFactory = DocumentPathFactory.from(context) + if (!DocumentsContract.isTreeUri(uri)) return null + val notPersisted = + context.contentResolverSafe.persistedUriPermissions.none { + it.uri == uri && it.isReadPermission && it.isWritePermission + } + if (notPersisted) return null + val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null + return MusicLocation(uri, path) + } + + fun toString(list: List) = + list.joinToString(";") { it.uri.toString().replace(";", "\\;") } + + fun existing(context: Context, string: String): List { + return string.splitEscaped { it == ';' }.mapNotNull { existing(context, Uri.parse(it)) } + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt new file mode 100644 index 000000000..f889d516b --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2024 Auxio Project + * Path.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs + +import android.content.Context +import java.io.File + +/** + * An abstraction of an android file system path, including the volume and relative path. + * + * @param volume The volume that the path is on. + * @param components The components of the path of the file, relative to the root of the volume. + */ +data class Path( + val volume: Volume, + val components: Components, +) { + /** The name of the file/directory. */ + val name: String? + get() = components.name + + /** The parent directory of the path, or itself if it's the root path. */ + val directory: Path + get() = Path(volume, components.parent()) + + /** + * Transforms this [Path] into a "file" of the given name that's within the "directory" + * represented by the current path. Ex. "/storage/emulated/0/Music" -> + * "/storage/emulated/0/Music/file.mp3" + * + * @param fileName The name of the file to append to the path. + * @return The new [Path] instance. + */ + fun file(fileName: String) = Path(volume, components.child(fileName)) + + /** + * Resolves the [Path] in a human-readable format. + * + * @param context [Context] required to obtain human-readable strings. + */ + fun resolve(context: Context) = "${volume.resolveName(context)}/$components" +} + +sealed interface Volume { + /** The name of the volume as it appears in MediaStore. */ + val mediaStoreName: String? + + /** + * The components of the path to the volume, relative from the system root. Should not be used + * except for compatibility purposes. + */ + val components: Components? + + /** Resolves the name of the volume in a human-readable format. */ + fun resolveName(context: Context): String + + /** A volume representing the device's internal storage. */ + interface Internal : Volume + + /** A volume representing an external storage device, identified by a UUID. */ + interface External : Volume { + /** The UUID of the volume. */ + val id: String? + } +} + +/** + * The components of a path. This allows the path to be manipulated without having tp handle + * separator parsing. + * + * @param components The components of the path. + */ +@JvmInline +value class Components private constructor(val components: List) { + /** The name of the file/directory. */ + val name: String? + get() = components.lastOrNull() + + override fun toString() = unixString + + /** Formats these components using the unix file separator (/) */ + val unixString: String + get() = components.joinToString(File.separator) + + /** Formats these components using the windows file separator (\). */ + val windowsString: String + get() = components.joinToString("\\") + + /** + * Returns a new [Components] instance with the last element of the path removed as a "parent" + * element of the original instance. + * + * @return The new [Components] instance, or the original instance if it's the root path. + */ + fun parent() = Components(components.dropLast(1)) + + /** + * Returns a new [Components] instance with the given name appended to the end of the path as a + * "child" element of the original instance. + * + * @param name The name of the file/directory to append to the path. + */ + fun child(name: String) = + if (name.isNotEmpty()) { + Components(components + name.trimSlashes()) + } else { + this + } + + /** + * Removes the first [n] elements of the path, effectively resulting in a path that is n levels + * deep. + * + * @param n The number of elements to remove. + * @return The new [Components] instance. + */ + fun depth(n: Int) = Components(components.drop(n)) + + /** + * Concatenates this [Components] instance with another. + * + * @param other The [Components] instance to concatenate with. + * @return The new [Components] instance. + */ + fun child(other: Components) = Components(components + other.components) + + /** + * Returns the given [Components] has a prefix equal to this [Components] instance. Effectively, + * as if the given [Components] instance was a child of this [Components] instance. + */ + fun contains(other: Components): Boolean { + if (other.components.size < components.size) { + return false + } + + return components == other.components.take(components.size) + } + + fun containing(other: Components) = Components(other.components.drop(components.size)) + + internal companion object { + /** + * Parses a path string into a [Components] instance by the unix path separator (/). + * + * @param path The path string to parse. + * @return The [Components] instance. + */ + fun parseUnix(path: String) = + Components(path.trimSlashes().split(File.separatorChar).filter { it.isNotEmpty() }) + + /** + * Parses a path string into a [Components] instance by the windows path separator. + * + * @param path The path string to parse. + * @return The [Components] instance. + */ + fun parseWindows(path: String) = + Components(path.trimSlashes().split('\\').filter { it.isNotEmpty() }) + + private fun String.trimSlashes() = trimStart(File.separatorChar).trimEnd(File.separatorChar) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt new file mode 100644 index 000000000..9c8f2a407 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 Auxio Project + * AppFiles.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs.app + +import android.os.ParcelFileDescriptor +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +interface AppFiles { + suspend fun find(name: String): AppFile? + + suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile + + suspend fun deleteWhere(block: (String) -> Boolean) + + companion object { + suspend fun at(dir: File): AppFiles { + withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } + return AppFilesImpl(dir) + } + } +} + +interface AppFile { + suspend fun fd(): ParcelFileDescriptor? + + suspend fun open(): InputStream? +} + +private class AppFilesImpl(private val dir: File) : AppFiles { + private val fileMutexes = mutableMapOf() + private val mapMutex = Mutex() + + private suspend fun getMutexForFile(file: String): Mutex { + return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } } + } + + override suspend fun find(name: String): AppFile? = + withContext(Dispatchers.IO) { + try { + File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) } + } catch (e: IOException) { + null + } + } + + override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile { + val fileMutex = getMutexForFile(name) + return fileMutex.withLock { + val targetFile = File(dir, name) + if (!targetFile.exists()) { + withContext(Dispatchers.IO) { + val tempFile = File(dir, "$name.tmp") + + try { + tempFile.outputStream().use { block(it) } + tempFile.renameTo(targetFile) + AppFileImpl(targetFile) + } catch (e: IOException) { + tempFile.delete() + throw e + } + } + } else { + AppFileImpl(targetFile) + } + } + } + + override suspend fun deleteWhere(block: (String) -> Boolean) { + withContext(Dispatchers.IO) { + dir.listFiles { file -> block(file.name) }?.forEach { it.deleteRecursively() } + } + } +} + +private data class AppFileImpl(private val file: File) : AppFile { + override suspend fun fd() = + withContext(Dispatchers.IO) { + try { + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + } catch (e: IOException) { + null + } + } + + override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt new file mode 100644 index 000000000..2f737995c --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 Auxio Project + * DeviceFiles.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs.device + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.flow +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.fs.MusicLocation +import org.oxycblt.musikr.fs.Path + +internal interface DeviceFiles { + fun explore(locations: Flow): Flow + + companion object { + fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe) + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { + override fun explore(locations: Flow): Flow = + locations.flatMapMerge { location -> + exploreImpl( + contentResolver, + location.uri, + DocumentsContract.getTreeDocumentId(location.uri), + location.path) + } + + private fun exploreImpl( + contentResolver: ContentResolver, + rootUri: Uri, + treeDocumentId: String, + relativePath: Path + ): Flow = flow { + contentResolver.useQuery( + DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), + PROJECTION) { cursor -> + val childUriIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val displayNameIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val mimeTypeIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) + val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) + val lastModifiedIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + val recursive = mutableListOf>() + while (cursor.moveToNext()) { + val childId = cursor.getString(childUriIndex) + val displayName = cursor.getString(displayNameIndex) + val newPath = relativePath.file(displayName) + val mimeType = cursor.getString(mimeTypeIndex) + if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { + // This does NOT block the current coroutine. Instead, we will + // evaluate this flow in parallel later to maximize throughput. + recursive.add(exploreImpl(contentResolver, rootUri, childId, newPath)) + } else if (mimeType.startsWith("audio/") && mimeType != "audio/x-mpegurl") { + // Immediately emit all files given that it's just an O(1) op. + // This also just makes sure the outer flow has a reason to exist + // rather than just being a glorified async. + val lastModified = cursor.getLong(lastModifiedIndex) + val size = cursor.getLong(sizeIndex) + emit( + DeviceFile( + DocumentsContract.buildDocumentUriUsingTree(rootUri, childId), + mimeType, + newPath, + size, + lastModified)) + } + } + emitAll(recursive.asFlow().flattenMerge()) + } + } + + private companion object { + val PROJECTION = + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt new file mode 100644 index 000000000..a305108d6 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Auxio Project + * QueryUtil.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs.device + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri + +/** + * Get a content resolver that will not mangle MediaStore queries on certain devices. See + * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. + */ +internal val Context.contentResolverSafe: ContentResolver + get() = applicationContext.contentResolver + +/** + * A shortcut for querying the [ContentResolver] database. + * + * @param uri The [Uri] of content to retrieve. + * @param projection A list of SQL columns to query from the database. + * @param selector A SQL selection statement to filter results. Spaces where arguments should be + * filled in are represented with a "?". + * @param args The arguments used for the selector. + * @return A [Cursor] of the queried values, organized by the column projection. + * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. + * @see ContentResolver.query + */ +internal fun ContentResolver.safeQuery( + uri: Uri, + projection: Array, + selector: String? = null, + args: Array? = null +) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" } + +/** + * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources + * when no longer used. + * + * @param uri The [Uri] of content to retrieve. + * @param projection A list of SQL columns to query from the database. + * @param selector A SQL selection statement to filter results. Spaces where arguments should be + * filled in are represented with a "?". + * @param args The arguments used for the selector. + * @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor] + * is empty. + * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. + * @see ContentResolver.query + */ +internal inline fun ContentResolver.useQuery( + uri: Uri, + projection: Array, + selector: String? = null, + args: Array? = null, + block: (Cursor) -> R +) = safeQuery(uri, projection, selector, args).use(block) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt index eb977a1fe..92b7f825f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/DocumentPathFactory.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt @@ -16,22 +16,25 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.musikr.fs.path import android.content.ContentUris import android.content.Context import android.net.Uri import android.provider.DocumentsContract -import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File -import javax.inject.Inject +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.fs.Volume +import org.oxycblt.musikr.fs.device.contentResolverSafe +import org.oxycblt.musikr.fs.device.useQuery /** * A factory for parsing the reverse-engineered format of the URIs obtained from document picker. * * @author Alexander Capehart (OxygenCobalt) */ -interface DocumentPathFactory { +internal interface DocumentPathFactory { /** * Unpacks a document URI into a [Path] instance, using [fromDocumentId]. * @@ -63,12 +66,18 @@ interface DocumentPathFactory { * @return The [Path] instance, or null if the path could not be deserialized. */ fun fromDocumentId(path: String): Path? + + companion object { + fun from(context: Context): DocumentPathFactory { + val volumeManager = VolumeManager.from(context) + val pathInterpreter = MediaStorePathInterpreter.Factory.from(volumeManager) + return DocumentPathFactoryImpl(context, volumeManager, pathInterpreter) + } + } } -class DocumentPathFactoryImpl -@Inject -constructor( - @ApplicationContext private val context: Context, +private class DocumentPathFactoryImpl( + private val context: Context, private val volumeManager: VolumeManager, private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory ) : DocumentPathFactory { diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt index f3a6f6aa4..d62d1cb45 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStorePathInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt @@ -16,19 +16,20 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.musikr.fs.path import android.database.Cursor import android.os.Build import android.provider.MediaStore -import org.oxycblt.auxio.util.logE +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.fs.Path /** * Wrapper around a [Cursor] that interprets path information on a per-API/manufacturer basis. * * @author Alexander Capehart (OxygenCobalt) */ -sealed interface MediaStorePathInterpreter { +internal sealed interface MediaStorePathInterpreter { /** * Extract a [Path] from the wrapped [Cursor]. This should be called after the cursor has been * moved to the row that should be interpreted. @@ -112,8 +113,6 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : } } - logE("Could not find volume for $data [tried: ${volumes.map { it.components }}]") - return null } @@ -181,8 +180,6 @@ private constructor(private val cursor: Cursor, volumeManager: VolumeManager) : val displayName = cursor.getString(displayNameIndex) val volume = volumes.find { it.mediaStoreName == volumeName } if (volume == null) { - logE( - "Could not find volume for $volumeName:$relativePath/$displayName [tried: ${volumes.map { it.mediaStoreName }}]") return null } val components = Components.parseUnix(relativePath).child(displayName) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeCompat.kt similarity index 51% rename from app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeCompat.kt index 6cdc7df67..2c0b87453 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeCompat.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2022 Auxio Project - * StorageUtil.kt is part of Auxio. + * Copyright (c) 2024 Auxio Project + * VolumeCompat.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,13 +16,10 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.musikr.fs.path import android.annotation.SuppressLint -import android.content.ContentResolver -import android.content.ContentUris import android.content.Context -import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Environment @@ -30,88 +27,8 @@ import android.os.storage.StorageManager import android.os.storage.StorageVolume import android.provider.MediaStore import java.lang.reflect.Method -import org.oxycblt.auxio.util.lazyReflectedMethod +import org.oxycblt.musikr.util.lazyReflectedMethod -// --- MEDIASTORE UTILITIES --- - -/** - * Get a content resolver that will not mangle MediaStore queries on certain devices. See - * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. - */ -val Context.contentResolverSafe: ContentResolver - get() = applicationContext.contentResolver - -/** - * A shortcut for querying the [ContentResolver] database. - * - * @param uri The [Uri] of content to retrieve. - * @param projection A list of SQL columns to query from the database. - * @param selector A SQL selection statement to filter results. Spaces where arguments should be - * filled in are represented with a "?". - * @param args The arguments used for the selector. - * @return A [Cursor] of the queried values, organized by the column projection. - * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. - * @see ContentResolver.query - */ -fun ContentResolver.safeQuery( - uri: Uri, - projection: Array, - selector: String? = null, - args: Array? = null -) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" } - -/** - * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources - * when no longer used. - * - * @param uri The [Uri] of content to retrieve. - * @param projection A list of SQL columns to query from the database. - * @param selector A SQL selection statement to filter results. Spaces where arguments should be - * filled in are represented with a "?". - * @param args The arguments used for the selector. - * @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor] - * is empty. - * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. - * @see ContentResolver.query - */ -inline fun ContentResolver.useQuery( - uri: Uri, - projection: Array, - selector: String? = null, - args: Array? = null, - block: (Cursor) -> R -) = safeQuery(uri, projection, selector, args).use(block) - -/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */ -private val externalCoversUri = Uri.parse("content://media/external/audio/albumart") - -/** - * Convert a [MediaStore] Song ID into a [Uri] to it's audio file. - * - * @return An external storage audio file [Uri]. May not exist. - * @see ContentUris.withAppendedId - * @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - */ -fun Long.toAudioUri() = - ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this) - -/** - * Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will - * be fast to load, but will be lower quality. - * - * @return An external storage image [Uri]. May not exist. - * @see ContentUris.withAppendedId - */ -fun Long.toSongCoverUri(): Uri = - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run { - appendPath(this@toSongCoverUri.toString()) - appendPath("albumart") - build() - } - -fun Long.toAlbumCoverUri(): Uri = ContentUris.withAppendedId(externalCoversUri, this) - -// --- STORAGEMANAGER UTILITIES --- // Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles /** @@ -123,23 +40,13 @@ fun Long.toAlbumCoverUri(): Uri = ContentUris.withAppendedId(externalCoversUri, @Suppress("NewApi") private val svApi21GetPathMethod: Method by lazyReflectedMethod(StorageVolume::class, "getPath") -/** - * The [StorageVolume] considered the "primary" volume by the system, obtained in a - * version-compatible manner. - * - * @see StorageManager.getPrimaryStorageVolume - * @see StorageVolume.isPrimary - */ -val StorageManager.primaryStorageVolumeCompat: StorageVolume - @Suppress("NewApi") get() = primaryStorageVolume - /** * The list of [StorageVolume]s currently recognized by [StorageManager], in a version-compatible * manner. * * @see StorageManager.getStorageVolumes */ -val StorageManager.storageVolumesCompat: List +internal val StorageManager.storageVolumesCompat: List get() = storageVolumes.toList() /** @@ -148,7 +55,7 @@ val StorageManager.storageVolumesCompat: List * * @see StorageVolume.getDirectory */ -val StorageVolume.directoryCompat: String? +internal val StorageVolume.directoryCompat: String? @SuppressLint("NewApi") get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -169,7 +76,7 @@ val StorageVolume.directoryCompat: String? * @return A human-readable name for this volume. */ @SuppressLint("NewApi") -fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context) +internal fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context) /** * If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May @@ -177,7 +84,7 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio * * @see StorageVolume.isPrimary */ -val StorageVolume.isPrimaryCompat: Boolean +internal val StorageVolume.isPrimaryCompat: Boolean @SuppressLint("NewApi") get() = isPrimary /** @@ -186,14 +93,14 @@ val StorageVolume.isPrimaryCompat: Boolean * * @see StorageVolume.isEmulated */ -val StorageVolume.isEmulatedCompat: Boolean +internal val StorageVolume.isEmulatedCompat: Boolean @SuppressLint("NewApi") get() = isEmulated /** * If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary" * to [MediaStore] and Document [Uri]s, obtained in a version compatible manner. */ -val StorageVolume.isInternalCompat: Boolean +internal val StorageVolume.isInternalCompat: Boolean // Must contain the android system AND be an emulated drive, as non-emulated system // volumes use their UUID instead of primary in MediaStore/Document URIs. get() = isPrimaryCompat && isEmulatedCompat @@ -204,7 +111,7 @@ val StorageVolume.isInternalCompat: Boolean * * @see StorageVolume.getUuid */ -val StorageVolume.uuidCompat: String? +internal val StorageVolume.uuidCompat: String? @SuppressLint("NewApi") get() = uuid /** @@ -213,7 +120,7 @@ val StorageVolume.uuidCompat: String? * * @see StorageVolume.getState */ -val StorageVolume.stateCompat: String +internal val StorageVolume.stateCompat: String @SuppressLint("NewApi") get() = state /** @@ -222,7 +129,7 @@ val StorageVolume.stateCompat: String * * @see StorageVolume.getMediaStoreVolumeName */ -val StorageVolume.mediaStoreVolumeNameCompat: String? +internal val StorageVolume.mediaStoreVolumeNameCompat: String? get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mediaStoreVolumeName diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt new file mode 100644 index 000000000..3e67ef5f7 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 Auxio Project + * VolumeManager.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.fs.path + +import android.content.Context +import android.os.storage.StorageManager +import android.os.storage.StorageVolume +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.fs.Volume + +/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */ +internal interface VolumeManager { + /** + * The internal storage volume of the device. + * + * @see StorageManager.getPrimaryStorageVolume + */ + fun getInternalVolume(): Volume.Internal + + /** + * The list of [Volume]s currently recognized by [StorageManager]. + * + * @see StorageManager.getStorageVolumes + */ + fun getVolumes(): List + + companion object { + fun from(context: Context): VolumeManager = + VolumeManagerImpl(context.getSystemService(StorageManager::class.java)) + } +} + +private class VolumeManagerImpl(private val storageManager: StorageManager) : VolumeManager { + override fun getInternalVolume(): Volume.Internal = + InternalVolumeImpl(storageManager.primaryStorageVolume) + + override fun getVolumes() = + storageManager.storageVolumesCompat.map { + if (it.isInternalCompat) { + InternalVolumeImpl(it) + } else { + ExternalVolumeImpl(it) + } + } + + private data class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal { + override val mediaStoreName + get() = storageVolume.mediaStoreVolumeNameCompat + + override val components + get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix) + + override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) + } + + private data class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External { + override val id + get() = storageVolume.uuidCompat + + override val mediaStoreName + get() = storageVolume.mediaStoreVolumeNameCompat + + override val components + get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix) + + override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt new file mode 100644 index 000000000..0d0e6104f --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2024 Auxio Project + * MusicGraph.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.graph + +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.playlist.SongPointer +import org.oxycblt.musikr.playlist.interpret.PrePlaylist +import org.oxycblt.musikr.tag.interpret.PreAlbum +import org.oxycblt.musikr.tag.interpret.PreArtist +import org.oxycblt.musikr.tag.interpret.PreGenre +import org.oxycblt.musikr.tag.interpret.PreSong +import org.oxycblt.musikr.util.unlikelyToBeNull + +internal data class MusicGraph( + val songVertex: List, + val albumVertex: List, + val artistVertex: List, + val genreVertex: List, + val playlistVertex: Set +) { + interface Builder { + fun add(preSong: PreSong) + + fun add(prePlaylist: PrePlaylist) + + fun build(): MusicGraph + } + + companion object { + fun builder(): Builder = MusicGraphBuilderImpl() + } +} + +private class MusicGraphBuilderImpl : MusicGraph.Builder { + private val songVertices = mutableMapOf() + private val albumVertices = mutableMapOf() + private val artistVertices = mutableMapOf() + private val genreVertices = mutableMapOf() + private val playlistVertices = mutableSetOf() + + override fun add(preSong: PreSong) { + val uid = preSong.uid + if (songVertices.containsKey(uid)) { + return + } + + val songGenreVertices = + preSong.preGenres.map { preGenre -> + genreVertices.getOrPut(preGenre) { GenreVertex(preGenre) } + } + + val songArtistVertices = + preSong.preArtists.map { preArtist -> + artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) } + } + + val albumVertex = + albumVertices.getOrPut(preSong.preAlbum) { + // Albums themselves have their own parent artists that also need to be + // linked up. + val albumArtistVertices = + preSong.preAlbum.preArtists.map { preArtist -> + artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) } + } + val albumVertex = AlbumVertex(preSong.preAlbum, albumArtistVertices.toMutableList()) + // Album vertex is linked, now link artists back to album. + albumArtistVertices.forEach { artistVertex -> + artistVertex.albumVertices.add(albumVertex) + } + albumVertex + } + + val songVertex = + SongVertex( + preSong, + albumVertex, + songArtistVertices.toMutableList(), + songGenreVertices.toMutableList()) + albumVertex.songVertices.add(songVertex) + + songArtistVertices.forEach { artistVertex -> + artistVertex.songVertices.add(songVertex) + songGenreVertices.forEach { genreVertex -> + // Mutually link any new genres to the artist + artistVertex.genreVertices.add(genreVertex) + genreVertex.artistVertices.add(artistVertex) + } + } + + songGenreVertices.forEach { genreVertex -> genreVertex.songVertices.add(songVertex) } + + songVertices[uid] = songVertex + } + + override fun add(prePlaylist: PrePlaylist) { + playlistVertices.add(PlaylistVertex(prePlaylist)) + } + + override fun build(): MusicGraph { + val genreClusters = genreVertices.values.groupBy { it.preGenre.rawName?.lowercase() } + for (cluster in genreClusters.values) { + simplifyGenreCluster(cluster) + } + + val artistClusters = artistVertices.values.groupBy { it.preArtist.rawName?.lowercase() } + for (cluster in artistClusters.values) { + simplifyArtistCluster(cluster) + } + + val albumClusters = albumVertices.values.groupBy { it.preAlbum.rawName?.lowercase() } + for (cluster in albumClusters.values) { + simplifyAlbumCluster(cluster) + } + + // Remove any edges that wound up connecting to the same artist or genre + // in the end after simplification. + albumVertices.values.forEach { + it.artistVertices = it.artistVertices.distinct().toMutableList() + } + + songVertices.entries.forEach { entry -> + val vertex = entry.value + vertex.artistVertices = vertex.artistVertices.distinct().toMutableList() + vertex.genreVertices = vertex.genreVertices.distinct().toMutableList() + + playlistVertices.forEach { + val pointer = SongPointer.UID(entry.key) + it.pointerMap[pointer]?.forEach { index -> it.songVertices[index] = vertex } + } + } + + val graph = + MusicGraph( + songVertices.values.toList(), + albumVertices.values.toList(), + artistVertices.values.toList(), + genreVertices.values.toList(), + playlistVertices) + + return graph + } + + private fun simplifyGenreCluster(cluster: Collection) { + if (cluster.size == 1) { + // Nothing to do. + return + } + // All of these genres are semantically equivalent. Pick the most popular variation + // and merge all the others into it. + val clusterSet = cluster.toMutableSet() + val dst = clusterSet.maxBy { it.songVertices.size } + clusterSet.remove(dst) + for (src in clusterSet) { + meldGenreVertices(src, dst) + } + } + + private fun meldGenreVertices(src: GenreVertex, dst: GenreVertex) { + if (src == dst) { + // Same vertex, do nothing + return + } + // Link all songs and artists from the irrelevant genre to the relevant genre. + dst.songVertices.addAll(src.songVertices) + dst.artistVertices.addAll(src.artistVertices) + // Update all songs and artists to point to the relevant genre. + src.songVertices.forEach { + val index = it.genreVertices.indexOf(src) + check(index >= 0) { "Illegal state: directed edge between genre and song" } + it.genreVertices[index] = dst + } + src.artistVertices.forEach { + it.genreVertices.remove(src) + it.genreVertices.add(dst) + } + // Remove the irrelevant genre from the graph. + genreVertices.remove(src.preGenre) + } + + private fun simplifyArtistCluster(cluster: Collection) { + if (cluster.size == 1) { + // Nothing to do. + return + } + val fullMusicBrainzIdCoverage = cluster.all { it.preArtist.musicBrainzId != null } + if (fullMusicBrainzIdCoverage) { + // All artists have MBIDs, nothing needs to be merged. + val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preArtist.musicBrainzId) } + for (mbidCluster in mbidClusters.values) { + simplifyArtistClusterImpl(mbidCluster) + } + return + } + // No full MBID coverage, discard the MBIDs from the graph. + val strippedCluster = + cluster.map { + val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null) + val simpleMbidVertex = + artistVertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) } + meldArtistVertices(it, simpleMbidVertex) + simpleMbidVertex + } + simplifyArtistClusterImpl(strippedCluster) + } + + private fun simplifyArtistClusterImpl(cluster: Collection) { + if (cluster.size == 1) { + // One canonical artist, nothing to collapse + return + } + val clusterSet = cluster.toMutableSet() + val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size } + clusterSet.remove(relevantArtistVertex) + for (irrelevantArtistVertex in clusterSet) { + meldArtistVertices(irrelevantArtistVertex, relevantArtistVertex) + } + } + + private fun meldArtistVertices(src: ArtistVertex, dst: ArtistVertex) { + if (src == dst) { + // Same vertex, do nothing + return + } + // Link all songs and albums from the irrelevant artist to the relevant artist. + dst.songVertices.addAll(src.songVertices) + dst.albumVertices.addAll(src.albumVertices) + dst.genreVertices.addAll(src.genreVertices) + // Update all songs, albums, and genres to point to the relevant artist. + src.songVertices.forEach { + // There can be duplicate artist vertices that we need to + // all replace when melding. + for (idx in it.artistVertices.indices) { + if (it.artistVertices[idx] == src) { + it.artistVertices[idx] = dst + } + } + } + src.albumVertices.forEach { + // There can be duplicate artist vertices that we need to + // all replace when melding. + for (idx in it.artistVertices.indices) { + if (it.artistVertices[idx] == src) { + it.artistVertices[idx] = dst + } + } + } + src.genreVertices.forEach { + it.artistVertices.remove(src) + it.artistVertices.add(dst) + } + + // Remove the irrelevant artist from the graph. + artistVertices.remove(src.preArtist) + } + + private fun simplifyAlbumCluster(cluster: Collection) { + if (cluster.size == 1) { + // Nothing to do. + return + } + val fullMusicBrainzIdCoverage = cluster.all { it.preAlbum.musicBrainzId != null } + if (fullMusicBrainzIdCoverage) { + // All albums have MBIDs, nothing needs to be merged. + val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preAlbum.musicBrainzId) } + for (mbidCluster in mbidClusters.values) { + simplifyAlbumClusterImpl(mbidCluster) + } + return + } + // No full MBID coverage, discard the MBIDs from the graph. + val strippedCluster = + cluster.map { + val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null) + val simpleMbidVertex = + albumVertices.getOrPut(noMbidPreAlbum) { + AlbumVertex(noMbidPreAlbum, it.artistVertices.toMutableList()) + } + meldAlbumVertices(it, simpleMbidVertex) + simpleMbidVertex + } + simplifyAlbumClusterImpl(strippedCluster) + } + + private fun simplifyAlbumClusterImpl(cluster: Collection) { + // All of these albums are semantically equivalent. Pick the most popular variation + // and merge all the others into it. + if (cluster.size == 1) { + // Nothing to do. + return + } + val clusterSet = cluster.toMutableSet() + val dst = clusterSet.maxBy { it.songVertices.size } + clusterSet.remove(dst) + for (src in clusterSet) { + meldAlbumVertices(src, dst) + } + } + + private fun meldAlbumVertices(src: AlbumVertex, dst: AlbumVertex) { + if (src == dst) { + // Same vertex, do nothing + return + } + // Link all songs and artists from the irrelevant album to the relevant album. + dst.songVertices.addAll(src.songVertices) + dst.artistVertices.addAll(src.artistVertices) + // Update all songs and artists to point to the relevant album. + src.songVertices.forEach { it.albumVertex = dst } + src.artistVertices.forEach { + it.albumVertices.remove(src) + it.albumVertices.add(dst) + } + // Remove the irrelevant album from the graph. + albumVertices.remove(src.preAlbum) + } +} + +internal interface Vertex { + val tag: Any? +} + +internal class SongVertex( + val preSong: PreSong, + var albumVertex: AlbumVertex, + var artistVertices: MutableList, + var genreVertices: MutableList +) : Vertex { + override var tag: Any? = null + + override fun toString() = "SongVertex(preSong=$preSong)" +} + +internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList) : + Vertex { + val songVertices = mutableSetOf() + override var tag: Any? = null + + override fun toString() = "AlbumVertex(preAlbum=$preAlbum)" +} + +internal class ArtistVertex( + val preArtist: PreArtist, +) : Vertex { + val songVertices = mutableSetOf() + val albumVertices = mutableSetOf() + val genreVertices = mutableSetOf() + override var tag: Any? = null + + override fun toString() = "ArtistVertex(preArtist=$preArtist)" +} + +internal class GenreVertex(val preGenre: PreGenre) : Vertex { + val songVertices = mutableSetOf() + val artistVertices = mutableSetOf() + override var tag: Any? = null + + override fun toString() = "GenreVertex(preGenre=$preGenre)" +} + +internal class PlaylistVertex(val prePlaylist: PrePlaylist) { + val songVertices = Array(prePlaylist.songPointers.size) { null } + val pointerMap = + prePlaylist.songPointers + .withIndex() + .groupBy { it.value } + .mapValuesTo(mutableMapOf()) { entry -> entry.value.map { it.index } } + val tag: Any? = null +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt new file mode 100644 index 000000000..758e4483a --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Auxio Project + * Metadata.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.metadata + +internal data class Metadata( + val id3v2: Map>, + val xiph: Map>, + val mp4: Map>, + val cover: ByteArray?, + val properties: Properties +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Metadata + + if (id3v2 != other.id3v2) return false + if (xiph != other.xiph) return false + if (mp4 != other.mp4) return false + if (cover != null) { + if (other.cover == null) return false + if (!cover.contentEquals(other.cover)) return false + } else if (other.cover != null) return false + if (properties != other.properties) return false + + return true + } + + override fun hashCode(): Int { + var result = id3v2.hashCode() + result = 31 * result + xiph.hashCode() + result = 31 * result + mp4.hashCode() + result = 31 * result + (cover?.contentHashCode() ?: 0) + result = 31 * result + properties.hashCode() + return result + } +} + +internal data class Properties( + val mimeType: String, + val durationMs: Long, + val bitrateKbps: Int, + val sampleRateHz: Int, +) diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt new file mode 100644 index 000000000..0b0cfc48d --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Auxio Project + * MetadataExtractor.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.metadata + +import android.os.ParcelFileDescriptor +import java.io.FileInputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.oxycblt.musikr.fs.DeviceFile + +internal interface MetadataExtractor { + suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata? + + companion object { + fun new(): MetadataExtractor = MetadataExtractorImpl + } +} + +private object MetadataExtractorImpl : MetadataExtractor { + override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) = + withContext(Dispatchers.IO) { + val fis = FileInputStream(fd.fileDescriptor) + TagLibJNI.open(deviceFile, fis).also { fis.close() } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt new file mode 100644 index 000000000..b8c22fb24 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 Auxio Project + * NativeInputStream.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.metadata + +import android.util.Log +import java.io.FileInputStream +import java.nio.ByteBuffer +import org.oxycblt.musikr.fs.DeviceFile + +internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) { + private val channel = fis.channel + + fun name() = requireNotNull(deviceFile.path.name) + + fun readBlock(buf: ByteBuffer): Boolean { + try { + channel.read(buf) + return true + } catch (e: Exception) { + Log.d("NativeInputStream", "Error reading block", e) + return false + } + } + + fun isOpen(): Boolean { + return channel.isOpen + } + + fun seekFromBeginning(offset: Long): Boolean { + try { + channel.position(offset) + return true + } catch (e: Exception) { + Log.d("NativeInputStream", "Error seeking from beginning", e) + return false + } + } + + fun seekFromCurrent(offset: Long): Boolean { + try { + channel.position(channel.position() + offset) + return true + } catch (e: Exception) { + Log.d("NativeInputStream", "Error seeking from current", e) + return false + } + } + + fun seekFromEnd(offset: Long): Boolean { + try { + channel.position(channel.size() + offset) + return true + } catch (e: Exception) { + Log.d("NativeInputStream", "Error seeking from end", e) + return false + } + } + + fun tell() = + try { + channel.position() + } catch (e: Exception) { + Log.d("NativeInputStream", "Error getting position", e) + Long.MIN_VALUE + } + + fun length() = + try { + channel.size() + } catch (e: Exception) { + Log.d("NativeInputStream", "Error getting length", e) + Long.MIN_VALUE + } + + fun close() { + channel.close() + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt new file mode 100644 index 000000000..0e04519b2 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeTagMap.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Auxio Project + * NativeTagMap.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.metadata + +import org.oxycblt.musikr.util.correctWhitespace + +internal class NativeTagMap { + private val map = mutableMapOf>() + + fun addID(id: String, value: String) { + addID(id, listOf(value)) + } + + fun addID(id: String, values: List) { + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map[id] = correctedValues + } + + fun addCustom(description: String, value: String) { + addCustom(description, listOf(value)) + } + + fun addCustom(description: String, values: List) { + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map[description.uppercase()] = correctedValues + } + + fun addCombined(id: String, description: String, value: String) { + addCombined(id, description, listOf(value)) + } + + fun addCombined(id: String, description: String, values: List) { + if (values.isEmpty()) { + return + } + val correctedValues = values.mapNotNull { it.correctWhitespace() } + if (correctedValues.isEmpty()) { + return + } + map["$id:${description.uppercase()}"] = correctedValues + } + + fun getObject(): Map> { + return map + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt new file mode 100644 index 000000000..d5105f3a8 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagLibJNI.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.metadata + +import java.io.FileInputStream +import org.oxycblt.musikr.fs.DeviceFile + +internal object TagLibJNI { + init { + System.loadLibrary("tagJNI") + } + + /** + * Open a file and extract a tag. + * + * Note: This method is blocking and should be handled as such if calling from a coroutine. + */ + fun open(deviceFile: DeviceFile, fis: FileInputStream): Metadata? { + val inputStream = NativeInputStream(deviceFile, fis) + val tag = openNative(inputStream) + inputStream.close() + return tag + } + + private external fun openNative(inputStream: NativeInputStream): Metadata? +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt new file mode 100644 index 000000000..99817b6eb --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Auxio Project + * AlbumImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Album +import org.oxycblt.musikr.Artist +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.interpret.PreAlbum +import org.oxycblt.musikr.util.update + +internal interface AlbumCore { + val preAlbum: PreAlbum + val songs: Set + + fun resolveArtists(): List +} + +/** + * Library-backed implementation of [Album]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class AlbumImpl internal constructor(private val core: AlbumCore) : Album { + private val preAlbum = core.preAlbum + + override val uid = + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + preAlbum.musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.ALBUM, it) } + ?: Music.UID.auxio(Music.UID.Item.ALBUM) { + // Hash based on only names despite the presence of a date to increase stability. + // I don't know if there is any situation where an artist will have two albums with + // the exact same name, but if there is, I would love to know. + update(preAlbum.rawName) + update(preAlbum.preArtists.map { it.rawName }) + } + override val name = preAlbum.name + override val releaseType = preAlbum.releaseType + override val durationMs = core.songs.sumOf { it.durationMs } + override val addedMs = core.songs.minOf { it.addedMs } + override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) + override val dates: Date.Range? = + core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } + + override val artists: List + get() = core.resolveArtists() + + override val songs = core.songs + + private val hashCode = 31 * (31 * uid.hashCode() + preAlbum.hashCode()) + songs.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is AlbumImpl && uid == other.uid && preAlbum == other.preAlbum && songs == other.songs + + override fun toString() = "Album(uid=$uid, name=$name)" + + fun a(other: AlbumImpl) = uid == other.uid +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt new file mode 100644 index 000000000..e05740401 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Auxio Project + * ArtistImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Album +import org.oxycblt.musikr.Artist +import org.oxycblt.musikr.Genre +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.tag.interpret.PreArtist +import org.oxycblt.musikr.util.update + +internal interface ArtistCore { + val preArtist: PreArtist + val songs: Set + val albums: Set + + fun resolveGenres(): Set +} + +/** + * Library-backed implementation of [Artist]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +internal class ArtistImpl(private val core: ArtistCore) : Artist { + override val uid = + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + core.preArtist.musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.ARTIST, it) } + ?: Music.UID.auxio(Music.UID.Item.ARTIST) { update(core.preArtist.rawName) } + override val name = core.preArtist.name + + override val songs = core.songs + override var explicitAlbums = core.albums + override var implicitAlbums = core.songs.mapTo(mutableSetOf()) { it.album } - core.albums + + override val genres: List + get() = core.resolveGenres().toList() + + override val durationMs = core.songs.sumOf { it.durationMs } + override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) + + private val hashCode = + 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is ArtistImpl && + uid == other.uid && + core.preArtist == other.core.preArtist && + songs == other.songs + + override fun toString() = "Artist(uid=$uid, name=$name)" +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt new file mode 100644 index 000000000..0805f284b --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Auxio Project + * GenreImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Artist +import org.oxycblt.musikr.Genre +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.tag.interpret.PreGenre +import org.oxycblt.musikr.util.update + +internal interface GenreCore { + val preGenre: PreGenre + val songs: Set + val artists: Set +} + +/** + * Library-backed implementation of [Genre]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +internal class GenreImpl(private val core: GenreCore) : Genre { + override val uid = Music.UID.auxio(Music.UID.Item.GENRE) { update(core.preGenre.rawName) } + override val name = core.preGenre.name + + override val songs = core.songs + override val artists = core.artists + override val durationMs = core.songs.sumOf { it.durationMs } + override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) + + private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is GenreImpl && + uid == other.uid && + core.preGenre == other.core.preGenre && + songs == other.songs + + override fun toString() = "Genre(uid=$uid, name=$name)" +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt new file mode 100644 index 000000000..92296c80c --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryFactory.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 Auxio Project + * LibraryFactory.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Album +import org.oxycblt.musikr.Artist +import org.oxycblt.musikr.Genre +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.MutableLibrary +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.graph.AlbumVertex +import org.oxycblt.musikr.graph.ArtistVertex +import org.oxycblt.musikr.graph.GenreVertex +import org.oxycblt.musikr.graph.MusicGraph +import org.oxycblt.musikr.graph.PlaylistVertex +import org.oxycblt.musikr.graph.SongVertex +import org.oxycblt.musikr.graph.Vertex +import org.oxycblt.musikr.playlist.db.StoredPlaylists +import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter + +internal interface LibraryFactory { + fun create( + graph: MusicGraph, + storedPlaylists: StoredPlaylists, + playlistInterpreter: PlaylistInterpreter + ): MutableLibrary + + companion object { + fun new(): LibraryFactory = LibraryFactoryImpl() + } +} + +private class LibraryFactoryImpl() : LibraryFactory { + override fun create( + graph: MusicGraph, + storedPlaylists: StoredPlaylists, + playlistInterpreter: PlaylistInterpreter + ): MutableLibrary { + val songs = + graph.songVertex.mapTo(mutableSetOf()) { vertex -> + SongImpl(SongVertexCore(vertex)).also { vertex.tag = it } + } + val albums = + graph.albumVertex.mapTo(mutableSetOf()) { vertex -> + AlbumImpl(AlbumVertexCore(vertex)).also { vertex.tag = it } + } + val artists = + graph.artistVertex.mapTo(mutableSetOf()) { vertex -> + ArtistImpl(ArtistVertexCore(vertex)).also { vertex.tag = it } + } + val genres = + graph.genreVertex.mapTo(mutableSetOf()) { vertex -> + GenreImpl(GenreVertexCore(vertex)).also { vertex.tag = it } + } + val playlists = + graph.playlistVertex.mapTo(mutableSetOf()) { vertex -> + PlaylistImpl(PlaylistVertexCore(vertex)) + } + return LibraryImpl( + songs, albums, artists, genres, playlists, storedPlaylists, playlistInterpreter) + } + + private class SongVertexCore(private val vertex: SongVertex) : SongCore { + override val preSong = vertex.preSong + + override fun resolveAlbum(): Album = tag(vertex.albumVertex) + + override fun resolveArtists(): List = vertex.artistVertices.map { tag(it) } + + override fun resolveGenres(): List = vertex.genreVertices.map { tag(it) } + } + + private class AlbumVertexCore(private val vertex: AlbumVertex) : AlbumCore { + override val preAlbum = vertex.preAlbum + + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } + + override fun resolveArtists(): List = vertex.artistVertices.map { tag(it) } + } + + private class ArtistVertexCore(private val vertex: ArtistVertex) : ArtistCore { + override val preArtist = vertex.preArtist + + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } + + override val albums: Set = vertex.albumVertices.mapTo(mutableSetOf()) { tag(it) } + + override fun resolveGenres(): Set = + vertex.genreVertices.mapTo(mutableSetOf()) { tag(it) } + } + + private class GenreVertexCore(vertex: GenreVertex) : GenreCore { + override val preGenre = vertex.preGenre + + override val songs: Set = vertex.songVertices.mapTo(mutableSetOf()) { tag(it) } + + override val artists: Set = vertex.artistVertices.mapTo(mutableSetOf()) { tag(it) } + } + + private class PlaylistVertexCore(vertex: PlaylistVertex) : PlaylistCore { + override val prePlaylist = vertex.prePlaylist + + override val songs: List = + vertex.songVertices.mapNotNull { vertex -> vertex?.let { tag(it) } } + } + + private companion object { + private inline fun tag(vertex: Vertex): T { + val tag = vertex.tag + check(tag is T) { "Dead Vertex Detected: $vertex" } + return tag + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt new file mode 100644 index 000000000..badcada45 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 Auxio Project + * LibraryImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.MutableLibrary +import org.oxycblt.musikr.Playlist +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.playlist.db.StoredPlaylists +import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter +import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo + +internal data class LibraryImpl( + override val songs: Collection, + override val albums: Collection, + override val artists: Collection, + override val genres: Collection, + override val playlists: Collection, + private val storedPlaylists: StoredPlaylists, + private val playlistInterpreter: PlaylistInterpreter +) : MutableLibrary { + private val songUidMap = songs.associateBy { it.uid } + private val albumUidMap = albums.associateBy { it.uid } + private val artistUidMap = artists.associateBy { it.uid } + private val genreUidMap = genres.associateBy { it.uid } + private val playlistUidMap = playlists.associateBy { it.uid } + + override fun empty() = songs.isEmpty() + + override fun findSong(uid: Music.UID) = songUidMap[uid] + + override fun findSongByPath(path: Path) = songs.find { it.path == path } + + override fun findAlbum(uid: Music.UID) = albumUidMap[uid] + + override fun findArtist(uid: Music.UID) = artistUidMap[uid] + + override fun findGenre(uid: Music.UID) = genreUidMap[uid] + + override fun findPlaylist(uid: Music.UID) = playlistUidMap[uid] + + override fun findPlaylistByName(name: String) = playlists.find { it.name.raw == name } + + override suspend fun createPlaylist(name: String, songs: List): MutableLibrary { + val handle = storedPlaylists.new(name, songs) + val postPlaylist = playlistInterpreter.interpret(name, handle) + val core = NewPlaylistCore(postPlaylist, songs) + val playlist = PlaylistImpl(core) + return copy(playlists = playlists + playlist) + } + + override suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary { + val playlistImpl = + requireNotNull(playlistUidMap[playlist.uid]) { + "Playlist to rename is not in this library" + } + val prePlaylist = playlistImpl.core.prePlaylist + prePlaylist.handle.rename(name) + val postPlaylist = playlistInterpreter.interpret(name, prePlaylist.handle) + val core = NewPlaylistCore(postPlaylist, playlist.songs) + val newPlaylist = PlaylistImpl(core) + return copy(playlists = playlists - playlistImpl + newPlaylist) + } + + override suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary { + val playlistImpl = + requireNotNull(playlistUidMap[playlist.uid]) { + "Playlist to add to is not in this library" + } + playlistImpl.core.prePlaylist.handle.add(songs) + val core = NewPlaylistCore(playlistImpl.core.prePlaylist, playlistImpl.songs + songs) + val newPlaylist = PlaylistImpl(core) + return copy(playlists = playlists - playlistImpl + newPlaylist) + } + + override suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary { + val playlistImpl = + requireNotNull(playlistUidMap[playlist.uid]) { + "Playlist to rewrite is not in this library" + } + playlistImpl.core.prePlaylist.handle.rewrite(songs) + val core = NewPlaylistCore(playlistImpl.core.prePlaylist, songs) + val newPlaylist = PlaylistImpl(core) + return copy(playlists = playlists - playlistImpl + newPlaylist) + } + + override suspend fun deletePlaylist(playlist: Playlist): MutableLibrary { + val playlistImpl = + requireNotNull(playlistUidMap[playlist.uid]) { + "Playlist to delete is not in this library" + } + playlistImpl.core.prePlaylist.handle.delete() + return copy(playlists = playlists - playlistImpl) + } + + private class NewPlaylistCore( + override val prePlaylist: PrePlaylistInfo, + override val songs: List + ) : PlaylistCore +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt new file mode 100644 index 000000000..a92197df5 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Auxio Project + * PlaylistImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Playlist +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo +import org.oxycblt.musikr.tag.Name + +internal interface PlaylistCore { + val prePlaylist: PrePlaylistInfo + val songs: List +} + +internal class PlaylistImpl(val core: PlaylistCore) : Playlist { + override val uid = core.prePlaylist.handle.uid + override val name: Name.Known = core.prePlaylist.name + override val durationMs = core.songs.sumOf { it.durationMs } + override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) + override val songs = core.songs + + private var hashCode = + 31 * (31 * uid.hashCode() + core.prePlaylist.hashCode()) + songs.hashCode() + + override fun equals(other: Any?) = + other is PlaylistImpl && core.prePlaylist == other.core.prePlaylist && songs == other.songs + + override fun hashCode() = hashCode + + override fun toString() = "Playlist(uid=$uid, name=$name)" +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt new file mode 100644 index 000000000..6a34168c6 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Auxio Project + * SongImpl.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.model + +import org.oxycblt.musikr.Album +import org.oxycblt.musikr.Artist +import org.oxycblt.musikr.Genre +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.tag.interpret.PreSong + +internal interface SongCore { + val preSong: PreSong + + fun resolveAlbum(): Album + + fun resolveArtists(): List + + fun resolveGenres(): List +} + +/** + * Library-backed implementation of [Song]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +internal class SongImpl(private val handle: SongCore) : Song { + private val preSong = handle.preSong + + override val uid = preSong.uid + override val name = preSong.name + override val track = preSong.track + override val disc = preSong.disc + override val date = preSong.date + override val uri = preSong.uri + override val path = preSong.path + override val format = preSong.format + override val size = preSong.size + override val durationMs = preSong.durationMs + override val bitrateKbps = preSong.bitrateKbps + override val sampleRateHz = preSong.sampleRateHz + override val replayGainAdjustment = preSong.replayGainAdjustment + override val modifiedMs = preSong.modifiedMs + override val addedMs = preSong.addedMs + override val cover = preSong.cover + override val album: Album + get() = handle.resolveAlbum() + + override val artists: List + get() = handle.resolveArtists() + + override val genres: List + get() = handle.resolveGenres() + + private val hashCode = 31 * uid.hashCode() + preSong.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is SongImpl && uid == other.uid && preSong == other.preSong + + override fun toString() = "Song(uid=$uid, name=$name)" +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt new file mode 100644 index 000000000..df4f72cb1 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Auxio Project + * EvaluateStep.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.pipeline + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import org.oxycblt.musikr.Interpretation +import org.oxycblt.musikr.MutableLibrary +import org.oxycblt.musikr.Storage +import org.oxycblt.musikr.graph.MusicGraph +import org.oxycblt.musikr.model.LibraryFactory +import org.oxycblt.musikr.playlist.db.StoredPlaylists +import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter +import org.oxycblt.musikr.tag.interpret.TagInterpreter + +internal interface EvaluateStep { + suspend fun evaluate(extractedMusic: Flow): MutableLibrary + + companion object { + fun new(storage: Storage, interpretation: Interpretation): EvaluateStep = + EvaluateStepImpl( + TagInterpreter.new(interpretation), + PlaylistInterpreter.new(interpretation), + storage.storedPlaylists, + LibraryFactory.new()) + } +} + +private class EvaluateStepImpl( + private val tagInterpreter: TagInterpreter, + private val playlistInterpreter: PlaylistInterpreter, + private val storedPlaylists: StoredPlaylists, + private val libraryFactory: LibraryFactory +) : EvaluateStep { + override suspend fun evaluate(extractedMusic: Flow): MutableLibrary { + val filterFlow = + extractedMusic.filterIsInstance().divert { + when (it) { + is ExtractedMusic.Valid.Song -> Divert.Right(it.song) + is ExtractedMusic.Valid.Playlist -> Divert.Left(it.file) + } + } + val rawSongs = filterFlow.right + val preSongs = + rawSongs + .map { wrap(it, tagInterpreter::interpret) } + .flowOn(Dispatchers.Default) + .buffer(Channel.UNLIMITED) + val prePlaylists = + filterFlow.left + .map { wrap(it, playlistInterpreter::interpret) } + .flowOn(Dispatchers.Default) + .buffer(Channel.UNLIMITED) + val graphBuilder = MusicGraph.builder() + val graphBuild = + merge( + filterFlow.manager, + preSongs.onEach { wrap(it, graphBuilder::add) }, + prePlaylists.onEach { wrap(it, graphBuilder::add) }) + graphBuild.collect() + val graph = graphBuilder.build() + return libraryFactory.create(graph, storedPlaylists, playlistInterpreter) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt new file mode 100644 index 000000000..1e77659e1 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExploreStep.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.pipeline + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import org.oxycblt.musikr.Storage +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.fs.MusicLocation +import org.oxycblt.musikr.fs.device.DeviceFiles +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.playlist.db.StoredPlaylists +import org.oxycblt.musikr.playlist.m3u.M3U + +internal interface ExploreStep { + fun explore(locations: List): Flow + + companion object { + fun from(context: Context, storage: Storage): ExploreStep = + ExploreStepImpl(DeviceFiles.from(context), storage.storedPlaylists) + } +} + +private class ExploreStepImpl( + private val deviceFiles: DeviceFiles, + private val storedPlaylists: StoredPlaylists +) : ExploreStep { + override fun explore(locations: List): Flow { + val audios = + deviceFiles + .explore(locations.asFlow()) + .mapNotNull { + when { + it.mimeType == M3U.MIME_TYPE -> null + it.mimeType.startsWith("audio/") -> ExploreNode.Audio(it) + else -> null + } + } + .flowOn(Dispatchers.IO) + .buffer() + val playlists = + flow { emitAll(storedPlaylists.read().asFlow()) } + .map { ExploreNode.Playlist(it) } + .flowOn(Dispatchers.IO) + .buffer() + return merge(audios, playlists) + } +} + +internal sealed interface ExploreNode { + data class Audio(val file: DeviceFile) : ExploreNode + + data class Playlist(val file: PlaylistFile) : ExploreNode +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt new file mode 100644 index 000000000..858421457 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExtractStep.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.pipeline + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.withContext +import org.oxycblt.musikr.Storage +import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cache.CacheResult +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.metadata.MetadataExtractor +import org.oxycblt.musikr.metadata.Properties +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.tag.parse.ParsedTags +import org.oxycblt.musikr.tag.parse.TagParser + +internal interface ExtractStep { + fun extract(nodes: Flow): Flow + + companion object { + fun from(context: Context, storage: Storage): ExtractStep = + ExtractStepImpl( + context, + MetadataExtractor.new(), + TagParser.new(), + storage.cache, + storage.storedCovers) + } +} + +private class ExtractStepImpl( + private val context: Context, + private val metadataExtractor: MetadataExtractor, + private val tagParser: TagParser, + private val cacheFactory: Cache.Factory, + private val storedCovers: MutableCovers +) : ExtractStep { + @OptIn(ExperimentalCoroutinesApi::class) + override fun extract(nodes: Flow): Flow { + val cache = cacheFactory.open() + val addingMs = System.currentTimeMillis() + val filterFlow = + nodes.divert { + when (it) { + is ExploreNode.Audio -> Divert.Right(it.file) + is ExploreNode.Playlist -> Divert.Left(it.file) + } + } + val audioNodes = filterFlow.right + val playlistNodes = filterFlow.left.map { ExtractedMusic.Valid.Playlist(it) } + + val readDistributedFlow = audioNodes.distribute(8) + val cacheResults = + readDistributedFlow.flows + .map { flow -> + flow + .map { wrap(it) { file -> cache.read(file, storedCovers) } } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + } + .flattenMerge() + .buffer(Channel.UNLIMITED) + val cacheFlow = + cacheResults.divert { + when (it) { + is CacheResult.Hit -> Divert.Left(it.song) + is CacheResult.Miss -> Divert.Right(it.file) + } + } + val cachedSongs = cacheFlow.left.map { ExtractedMusic.Valid.Song(it) } + val uncachedSongs = cacheFlow.right + + val fds = + uncachedSongs + .mapNotNull { + wrap(it) { file -> + withContext(Dispatchers.IO) { + context.contentResolver.openFileDescriptor(file.uri, "r")?.let { fd -> + FileWith(file, fd) + } + } + } + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + + val metadata = + fds.mapNotNull { fileWith -> + wrap(fileWith.file) { _ -> + metadataExtractor + .extract(fileWith.file, fileWith.with) + .let { FileWith(fileWith.file, it) } + .also { withContext(Dispatchers.IO) { fileWith.with.close() } } + } + } + .flowOn(Dispatchers.IO) + // Covers are pretty big, so cap the amount of parsed metadata in-memory to at most + // 8 to minimize GCs. + .buffer(8) + + val extractedSongs = + metadata + .map { fileWith -> + if (fileWith.with != null) { + val tags = tagParser.parse(fileWith.file, fileWith.with) + val cover = fileWith.with.cover?.let { storedCovers.write(it) } + RawSong(fileWith.file, fileWith.with.properties, tags, cover, addingMs) + } else { + null + } + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + + val extractedFilter = + extractedSongs.divert { + if (it != null) Divert.Left(it) else Divert.Right(ExtractedMusic.Invalid) + } + + val write = extractedFilter.left + val invalid = extractedFilter.right + + val writeDistributedFlow = write.distribute(8) + val writtenSongs = + writeDistributedFlow.flows + .map { flow -> + flow + .map { + wrap(it, cache::write) + ExtractedMusic.Valid.Song(it) + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + } + .flattenMerge() + + val merged = + merge( + filterFlow.manager, + readDistributedFlow.manager, + cacheFlow.manager, + cachedSongs, + extractedFilter.manager, + writeDistributedFlow.manager, + writtenSongs, + invalid, + playlistNodes) + + return merged.onCompletion { cache.finalize() } + } + + private data class FileWith(val file: DeviceFile, val with: T) +} + +internal data class RawSong( + val file: DeviceFile, + val properties: Properties, + val tags: ParsedTags, + val cover: Cover?, + val addedMs: Long +) + +internal sealed interface ExtractedMusic { + sealed interface Valid : ExtractedMusic { + data class Song(val song: RawSong) : Valid + + data class Playlist(val file: PlaylistFile) : Valid + } + + data object Invalid : ExtractedMusic +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt new file mode 100644 index 000000000..58f2c6eb0 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Auxio Project + * FlowUtil.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.pipeline + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.withIndex + +internal sealed interface Divert { + data class Left(val value: L) : Divert + + data class Right(val value: R) : Divert +} + +internal class DivertedFlow( + val manager: Flow, + val left: Flow, + val right: Flow +) + +internal inline fun Flow.divert( + crossinline predicate: (T) -> Divert +): DivertedFlow { + val leftChannel = Channel(Channel.UNLIMITED) + val rightChannel = Channel(Channel.UNLIMITED) + val managedFlow = + flow { + collect { + when (val result = predicate(it)) { + is Divert.Left -> leftChannel.send(result.value) + is Divert.Right -> rightChannel.send(result.value) + } + } + leftChannel.close() + rightChannel.close() + } + return DivertedFlow(managedFlow, leftChannel.receiveAsFlow(), rightChannel.receiveAsFlow()) +} + +internal class DistributedFlow(val manager: Flow, val flows: Flow>) + +/** + * Equally "distributes" the values of some flow across n new flows. + * + * Note that this function requires the "manager" flow to be consumed alongside the split flows in + * order to function. Without this, all of the newly split flows will simply block. + */ +internal fun Flow.distribute(n: Int): DistributedFlow { + val posChannels = List(n) { Channel(Channel.UNLIMITED) } + val managerFlow = + flow { + withIndex().collect { + val index = it.index % n + posChannels[index].send(it.value) + } + for (channel in posChannels) { + channel.close() + } + } + val hotFlows = posChannels.asFlow().map { it.receiveAsFlow() } + return DistributedFlow(managerFlow, hotFlows) +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt new file mode 100644 index 000000000..1f6efc892 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 Auxio Project + * PipelineException.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.pipeline + +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.playlist.interpret.PrePlaylist +import org.oxycblt.musikr.tag.interpret.PreSong + +class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() { + override val cause = error + + override val message = "Error while processing ${processing}: ${error.stackTraceToString()}" +} + +sealed interface WhileProcessing { + class AFile internal constructor(private val file: DeviceFile) : WhileProcessing { + override fun toString() = "File @ ${file.path}" + } + + class ARawSong internal constructor(private val rawSong: RawSong) : WhileProcessing { + override fun toString() = "Raw Song @ ${rawSong.file.path}" + } + + class APlaylistFile internal constructor(private val playlist: PlaylistFile) : WhileProcessing { + override fun toString() = "Playlist File @ ${playlist.name}" + } + + class APreSong internal constructor(private val preSong: PreSong) : WhileProcessing { + override fun toString() = "Pre Song @ ${preSong.path}" + } + + class APrePlaylist internal constructor(private val prePlaylist: PrePlaylist) : + WhileProcessing { + override fun toString() = "Pre Playlist @ ${prePlaylist.name}" + } +} + +internal suspend fun wrap(file: DeviceFile, block: suspend (DeviceFile) -> R): R = + try { + block(file) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.AFile(file), e) + } + +internal suspend fun wrap(song: RawSong, block: suspend (RawSong) -> R): R = + try { + block(song) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.ARawSong(song), e) + } + +internal suspend fun wrap(file: PlaylistFile, block: suspend (PlaylistFile) -> R): R = + try { + block(file) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.APlaylistFile(file), e) + } + +internal suspend fun wrap(song: PreSong, block: suspend (PreSong) -> R): R = + try { + block(song) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.APreSong(song), e) + } + +internal suspend fun wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R = + try { + block(playlist) + } catch (e: Exception) { + throw PipelineException(WhileProcessing.APrePlaylist(playlist), e) + } diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt similarity index 71% rename from app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt rename to musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt index a1171efee..62efd7ca9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/ExternalPlaylistManager.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt @@ -16,18 +16,16 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.external +package org.oxycblt.musikr.playlist import android.content.Context import android.net.Uri -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.DocumentPathFactory -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.fs.contentResolverSafe -import org.oxycblt.auxio.util.logE +import org.oxycblt.musikr.Playlist +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.fs.device.contentResolverSafe +import org.oxycblt.musikr.fs.path.DocumentPathFactory +import org.oxycblt.musikr.playlist.m3u.M3U /** * Generic playlist file importing abstraction. @@ -38,7 +36,7 @@ import org.oxycblt.auxio.util.logE */ interface ExternalPlaylistManager { /** - * Import the playlist file at the given [uri]. + * Import the playli L.d("Unable to extract bit rate field") st file at the given [uri]. * * @param uri The [Uri] of the playlist file to import. * @return An [ImportedPlaylist] containing the paths to the files listed in the playlist file, @@ -55,6 +53,12 @@ interface ExternalPlaylistManager { * @return True if the playlist was successfully exported, false otherwise. */ suspend fun export(playlist: Playlist, uri: Uri, config: ExportConfig): Boolean + + companion object { + fun from(context: Context): ExternalPlaylistManager = + ExternalPlaylistManagerImpl( + context, DocumentPathFactory.from(context), M3U.from(context)) + } } /** @@ -80,10 +84,8 @@ data class ImportedPlaylist(val name: String?, val paths: List) typealias PossiblePaths = List -class ExternalPlaylistManagerImpl -@Inject -constructor( - @ApplicationContext private val context: Context, +private class ExternalPlaylistManagerImpl( + private val context: Context, private val documentPathFactory: DocumentPathFactory, private val m3u: M3U ) : ExternalPlaylistManager { @@ -92,10 +94,22 @@ constructor( return try { context.contentResolverSafe.openInputStream(uri)?.use { - return m3u.read(it, filePath.directory) + val imported = m3u.read(it, filePath.directory) ?: return null + val name = imported.name + if (name != null) { + return imported + } + // Strip extension + val fileName = filePath.name ?: return imported + val split = fileName.split(".") + var newName = split[0] + // Replace delimiters with space + newName = newName.replace(Regex("[_-]"), " ") + // Replace long stretches of whitespace with one space + newName = newName.replace(Regex("\\s+"), " ") + return ImportedPlaylist(newName, imported.paths) } } catch (e: Exception) { - logE("Failed to import playlist: $e") null } } @@ -109,17 +123,12 @@ constructor( filePath.directory } return try { - val outputStream = context.contentResolverSafe.openOutputStream(uri) - if (outputStream == null) { - logE("Failed to export playlist: Could not open output stream") - return false - } + val outputStream = context.contentResolverSafe.openOutputStream(uri) ?: return false outputStream.use { m3u.write(playlist, it, workingDirectory, config) true } } catch (e: Exception) { - logE("Failed to export playlist: $e") false } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/PlaylistFile.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/PlaylistFile.kt new file mode 100644 index 000000000..ad52037d6 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/PlaylistFile.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaylistFile.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.playlist + +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.Song + +internal data class PlaylistFile( + val name: String, + val songPointers: List, + val handle: PlaylistHandle +) + +internal sealed interface SongPointer { + data class UID(val uid: Music.UID) : SongPointer + // data class Path(val options: List) : SongPointer +} + +internal interface PlaylistHandle { + val uid: Music.UID + + suspend fun rename(name: String) + + suspend fun add(songs: List) + + suspend fun rewrite(songs: List) + + suspend fun delete() +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/PlaylistDatabase.kt similarity index 89% rename from app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt rename to musikr/src/main/java/org/oxycblt/musikr/playlist/db/PlaylistDatabase.kt index b6358ee3b..021b01595 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserMusicDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/PlaylistDatabase.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Auxio Project - * UserMusicDatabase.kt is part of Auxio. + * PlaylistDatabase.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,17 +16,19 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.user +package org.oxycblt.musikr.playlist.db +import android.content.Context import androidx.room.Dao import androidx.room.Database import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.Transaction import androidx.room.TypeConverters -import org.oxycblt.auxio.music.Music +import org.oxycblt.musikr.Music /** * Allows persistence of all user-created music information. @@ -38,8 +40,16 @@ import org.oxycblt.auxio.music.Music version = 30, exportSchema = false) @TypeConverters(Music.UID.TypeConverters::class) -abstract class UserMusicDatabase : RoomDatabase() { +internal abstract class PlaylistDatabase : RoomDatabase() { abstract fun playlistDao(): PlaylistDao + + companion object { + fun from(context: Context) = + Room.databaseBuilder( + context.applicationContext, PlaylistDatabase::class.java, "user_music.db") + .fallbackToDestructiveMigration() + .build() + } } // TODO: Handle playlist defragmentation? I really don't want dead songs to accumulate in this @@ -51,7 +61,7 @@ abstract class UserMusicDatabase : RoomDatabase() { * @author Alexander Capehart (OxygenCobalt) */ @Dao -abstract class PlaylistDao { +internal abstract class PlaylistDao { /** * Read out all playlists stored in the database. * diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/RawPlaylist.kt similarity index 81% rename from app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt rename to musikr/src/main/java/org/oxycblt/musikr/playlist/db/RawPlaylist.kt index 27cf62554..1353458be 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/RawPlaylist.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/RawPlaylist.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.user +package org.oxycblt.musikr.playlist.db import androidx.room.ColumnInfo import androidx.room.Embedded @@ -24,14 +24,14 @@ import androidx.room.Entity import androidx.room.Junction import androidx.room.PrimaryKey import androidx.room.Relation -import org.oxycblt.auxio.music.Music +import org.oxycblt.musikr.Music /** - * Raw playlist information persisted to [UserMusicDatabase]. + * Raw playlist information persisted to [PlaylistDatabase]. * * @author Alexander Capehart (OxygenCobalt) */ -data class RawPlaylist( +internal data class RawPlaylist( @Embedded val playlistInfo: PlaylistInfo, @Relation( parentColumn = "playlistUid", @@ -45,14 +45,14 @@ data class RawPlaylist( * * @author Alexander Capehart (OxygenCobalt) */ -@Entity data class PlaylistInfo(@PrimaryKey val playlistUid: Music.UID, val name: String) +@Entity internal data class PlaylistInfo(@PrimaryKey val playlistUid: Music.UID, val name: String) /** * Song information corresponding to a [RawPlaylist] entry. * * @author Alexander Capehart (OxygenCobalt) */ -@Entity data class PlaylistSong(@PrimaryKey val songUid: Music.UID) +@Entity internal data class PlaylistSong(@PrimaryKey val songUid: Music.UID) /** * Links individual songs to a playlist entry. @@ -60,7 +60,7 @@ data class RawPlaylist( * @author Alexander Capehart (OxygenCobalt) */ @Entity -data class PlaylistSongCrossRef( +internal data class PlaylistSongCrossRef( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(index = true) val playlistUid: Music.UID, @ColumnInfo(index = true) val songUid: Music.UID diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylistHandle.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylistHandle.kt new file mode 100644 index 000000000..36867202e --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylistHandle.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Auxio Project + * StoredPlaylistHandle.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.playlist.db + +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.playlist.PlaylistHandle + +internal class StoredPlaylistHandle( + private val playlistInfo: PlaylistInfo, + private val playlistDao: PlaylistDao +) : PlaylistHandle { + override val uid = playlistInfo.playlistUid + + override suspend fun rename(name: String) { + playlistDao.replacePlaylistInfo(playlistInfo.copy(name = name)) + } + + override suspend fun rewrite(songs: List) { + playlistDao.replacePlaylistSongs(uid, songs.map { PlaylistSong(it.uid) }) + } + + override suspend fun add(songs: List) { + playlistDao.insertPlaylistSongs(uid, songs.map { PlaylistSong(it.uid) }) + } + + override suspend fun delete() { + playlistDao.deletePlaylist(uid) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylists.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylists.kt new file mode 100644 index 000000000..44abce745 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/db/StoredPlaylists.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Auxio Project + * StoredPlaylists.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.playlist.db + +import android.content.Context +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.Song +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.playlist.PlaylistHandle +import org.oxycblt.musikr.playlist.SongPointer + +abstract class StoredPlaylists { + internal abstract suspend fun new(name: String, songs: List): PlaylistHandle + + internal abstract suspend fun read(): List + + companion object { + fun from(context: Context): StoredPlaylists = + StoredPlaylistsImpl(PlaylistDatabase.from(context).playlistDao()) + } +} + +private class StoredPlaylistsImpl(private val playlistDao: PlaylistDao) : StoredPlaylists() { + override suspend fun new(name: String, songs: List): PlaylistHandle { + val info = PlaylistInfo(Music.UID.auxio(Music.UID.Item.PLAYLIST), name) + playlistDao.insertPlaylist(RawPlaylist(info, songs.map { PlaylistSong(it.uid) })) + return StoredPlaylistHandle(info, playlistDao) + } + + override suspend fun read() = + playlistDao.readRawPlaylists().map { + PlaylistFile( + it.playlistInfo.name, + it.songs.map { song -> SongPointer.UID(song.songUid) }, + StoredPlaylistHandle(it.playlistInfo, playlistDao)) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PlaylistInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PlaylistInterpreter.kt new file mode 100644 index 000000000..2525a4141 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PlaylistInterpreter.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Auxio Project + * PlaylistInterpreter.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.playlist.interpret + +import org.oxycblt.musikr.Interpretation +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.playlist.PlaylistHandle +import org.oxycblt.musikr.tag.interpret.Naming + +internal interface PlaylistInterpreter { + fun interpret(file: PlaylistFile): PrePlaylist + + fun interpret(name: String, handle: PlaylistHandle): PostPlaylist + + companion object { + fun new(interpretation: Interpretation): PlaylistInterpreter = + PlaylistInterpreterImpl(interpretation.naming) + } +} + +private class PlaylistInterpreterImpl(private val naming: Naming) : PlaylistInterpreter { + override fun interpret(file: PlaylistFile) = + PrePlaylist( + name = naming.name(file.name, null), + rawName = file.name, + handle = file.handle, + songPointers = file.songPointers) + + override fun interpret(name: String, handle: PlaylistHandle): PostPlaylist = + PostPlaylist(name = naming.name(name, null), rawName = name, handle = handle) +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PrePlaylist.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PrePlaylist.kt new file mode 100644 index 000000000..7a16d23c4 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/interpret/PrePlaylist.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Auxio Project + * PrePlaylist.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.playlist.interpret + +import org.oxycblt.musikr.playlist.PlaylistHandle +import org.oxycblt.musikr.playlist.SongPointer +import org.oxycblt.musikr.tag.Name + +internal interface PrePlaylistInfo { + val name: Name.Known + val rawName: String? + val handle: PlaylistHandle +} + +internal data class PrePlaylist( + override val name: Name.Known, + override val rawName: String?, + override val handle: PlaylistHandle, + val songPointers: List +) : PrePlaylistInfo + +internal data class PostPlaylist( + override val name: Name.Known, + override val rawName: String?, + override val handle: PlaylistHandle, +) : PrePlaylistInfo diff --git a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt similarity index 85% rename from app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt rename to musikr/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt index 211f6cea6..be4bee901 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/external/M3U.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt @@ -16,32 +16,32 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.external +package org.oxycblt.musikr.playlist.m3u import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import java.io.BufferedReader import java.io.BufferedWriter import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream -import javax.inject.Inject -import org.oxycblt.auxio.music.Playlist -import org.oxycblt.auxio.music.fs.Components -import org.oxycblt.auxio.music.fs.Path -import org.oxycblt.auxio.music.fs.Volume -import org.oxycblt.auxio.music.fs.VolumeManager -import org.oxycblt.auxio.music.metadata.correctWhitespace -import org.oxycblt.auxio.music.resolveNames -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.unlikelyToBeNull +import org.oxycblt.musikr.Playlist +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.fs.Volume +import org.oxycblt.musikr.fs.path.VolumeManager +import org.oxycblt.musikr.playlist.ExportConfig +import org.oxycblt.musikr.playlist.ImportedPlaylist +import org.oxycblt.musikr.playlist.PossiblePaths +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.util.correctWhitespace +import org.oxycblt.musikr.util.unlikelyToBeNull /** * Minimal M3U file format implementation. * * @author Alexander Capehart (OxygenCobalt) */ -interface M3U { +abstract class M3U { /** * Reads an M3U file from the given [stream] and returns a [ImportedPlaylist] containing the * paths to the files listed in the M3U file. @@ -51,7 +51,7 @@ interface M3U { * resolve relative paths. * @return An [ImportedPlaylist] containing the paths to the files listed in the M3U file, */ - fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? + internal abstract fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? /** * Writes the given [playlist] to the given [outputStream] in the M3U format,. @@ -62,7 +62,7 @@ interface M3U { * create relative paths to where the M3U file is assumed to be stored. * @param config The configuration to use when exporting the playlist. */ - fun write( + internal abstract fun write( playlist: Playlist, outputStream: OutputStream, workingDirectory: Path, @@ -72,15 +72,12 @@ interface M3U { companion object { /** The mime type used for M3U files by the android system. */ const val MIME_TYPE = "audio/x-mpegurl" + + internal fun from(context: Context): M3U = M3UImpl(VolumeManager.from(context)) } } -class M3UImpl -@Inject -constructor( - @ApplicationContext private val context: Context, - private val volumeManager: VolumeManager -) : M3U { +private class M3UImpl(private val volumeManager: VolumeManager) : M3U() { override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? { val volumes = volumeManager.getVolumes() val reader = BufferedReader(InputStreamReader(stream)) @@ -115,15 +112,10 @@ constructor( } } - if (path == null) { - logE("Expected a path, instead got an EOF") - break@consumeFile - } - // There is basically no formal specification of file paths in M3U, and it differs // based on the programs that generated it. I more or less have to consider any possible // interpretation as valid. - val interpretations = interpretPath(path) + val interpretations = interpretPath(unlikelyToBeNull(path)) val possibilities = interpretations.flatMap { expandInterpretation(it, workingDirectory, volumes) } @@ -187,12 +179,26 @@ constructor( // I imagine other players will use. writer.writeLine("#EXTM3U") writer.writeLine("#EXTENC:UTF-8") - writer.writeLine("#PLAYLIST:${playlist.name.resolve(context)}") + writer.writeLine("#PLAYLIST:${playlist.name.raw}") for (song in playlist.songs) { - writer.writeLine("#EXTINF:${song.durationMs},${song.name.resolve(context)}") - writer.writeLine("#EXTALB:${song.album.name.resolve(context)}") - writer.writeLine("#EXTART:${song.artists.resolveNames(context)}") - writer.writeLine("#EXTGEN:${song.genres.resolveNames(context)}") + writer.writeLine("#EXTINF:${song.durationMs},${song.name.raw}") + val albumName = song.album.name + if (albumName is Name.Known) { + writer.writeLine("#EXTALB:${albumName.raw}") + } + // TODO: See if repeating #EXTART and #EXTGEN is legal + for (artist in song.artists) { + val name = artist.name + if (name is Name.Known) { + writer.writeLine("#EXTART:${name.raw}") + } + } + for (genre in song.genres) { + val name = genre.name + if (name is Name.Known) { + writer.writeLine("#EXTGEN:${name.raw}") + } + } val formattedPath = if (config.absolute) { @@ -261,7 +267,7 @@ constructor( } commonIndex == components.size -> { // The working directory is deeper in the path, backtrack. - for (i in 0..workingDirectory.components.size - commonIndex - 1) { + for (i in 0 ..< workingDirectory.components.size - commonIndex) { relativeComponents = relativeComponents.child("..") } } @@ -272,7 +278,7 @@ constructor( } else -> { // The paths are siblings. Backtrack and append as needed. - for (i in 0..workingDirectory.components.size - commonIndex - 1) { + for (i in 0 ..< workingDirectory.components.size - commonIndex) { relativeComponents = relativeComponents.child("..") } relativeComponents = relativeComponents.child(depth(commonIndex)) diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/Date.kt similarity index 78% rename from app/src/main/java/org/oxycblt/auxio/music/info/Date.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/Date.kt index 4098d0c37..1117e7a9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Date.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/Date.kt @@ -16,16 +16,11 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info +package org.oxycblt.musikr.tag -import android.content.Context -import java.text.ParseException -import java.text.SimpleDateFormat import kotlin.math.max -import org.oxycblt.auxio.R -import org.oxycblt.auxio.util.inRangeOrNull -import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.positiveOrNull +import org.oxycblt.musikr.util.inRangeOrNull +import org.oxycblt.musikr.util.positiveOrNull /** * An ISO-8601/RFC 3339 Date. @@ -40,38 +35,9 @@ class Date private constructor(private val tokens: List) : Comparable val year = tokens[0] val month = tokens.getOrNull(1) val day = tokens.getOrNull(2) - private val hour = tokens.getOrNull(3) - private val minute = tokens.getOrNull(4) - private val second = tokens.getOrNull(5) - - /** - * Resolve this instance into a human-readable date. - * - * @param context [Context] required to get human-readable names. - * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan - * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will - * be properly localized. - */ - fun resolve(context: Context) = - // Unable to create fine-grained date, just format as a year. - month?.let { resolveFineGrained() } ?: context.getString(R.string.fmt_number, year) - - private fun resolveFineGrained(): String? { - // We can't directly load a date with our own - val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) - format.applyPattern("yyyy-MM") - val date = - try { - format.parse("$year-$month") - } catch (e: ParseException) { - logE("Unable to parse fine-grained date: $e") - return null - } - - // Reformat as a readable month and year - format.applyPattern("MMM yyyy") - return format.format(date) - } + val hour = tokens.getOrNull(3) + val minute = tokens.getOrNull(4) + val second = tokens.getOrNull(5) override fun equals(other: Any?) = other is Date && compareTo(other) == 0 @@ -129,22 +95,6 @@ class Date private constructor(private val tokens: List) : Comparable check(min <= max) { "Min date must be <= max date" } } - /** - * Resolve this instance into a human-readable date range. - * - * @param context [Context] required to get human-readable names. - * @return If the date has a maximum value, then a `min - max` formatted string will be - * returned with the formatted [Date]s of the minimum and maximum dates respectively. - * Otherwise, the formatted name of the minimum [Date] will be returned. - */ - fun resolveDate(context: Context) = - if (min != max) { - context.getString( - R.string.fmt_date_range, min.resolve(context), max.resolve(context)) - } else { - min.resolve(context) - } - override fun equals(other: Any?) = other is Range && min == other.min && max == other.max override fun hashCode() = 31 * max.hashCode() + min.hashCode() diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/Disc.kt similarity index 76% rename from app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/Disc.kt index 5f2b52bd6..7ee67cd6c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Disc.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/Disc.kt @@ -16,11 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info - -import android.content.Context -import org.oxycblt.auxio.R -import org.oxycblt.auxio.list.Item +package org.oxycblt.musikr.tag /** * A disc identifier for a song. @@ -28,7 +24,7 @@ import org.oxycblt.auxio.list.Item * @param number The disc number. * @param name The name of the disc group, if any. Null if not present. */ -class Disc(val number: Int, val name: String?) : Item, Comparable { +class Disc(val number: Int, val name: String?) : Comparable { // We don't want to group discs by differing subtitles, so only compare by the number override fun equals(other: Any?) = other is Disc && number == other.number @@ -36,7 +32,3 @@ class Disc(val number: Int, val name: String?) : Item, Comparable { override fun compareTo(other: Disc) = number.compareTo(other.number) } - -fun Disc?.resolveNumber(context: Context) = - this?.run { context.getString(R.string.fmt_disc_no, number) } - ?: context.getString(R.string.def_disc) diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/Name.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/Name.kt new file mode 100644 index 000000000..c20ee72af --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/Name.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 Auxio Project + * Name.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag + +import java.text.CollationKey + +/** + * The name of a music item. + * + * This class automatically implements advanced sorting heuristics for music naming, + * + * @author Alexander Capehart + */ +sealed interface Name : Comparable { + /** A name that could be obtained for the music item. */ + abstract class Known : Name { + /** The raw name string obtained. Should be ignored in favor of [resolve]. */ + abstract val raw: String + /** The raw sort name string obtained. */ + abstract val sort: String? + /** A tokenized version of the name that will be compared. */ + abstract val tokens: List + + final override fun compareTo(other: Name) = + when (other) { + is Known -> { + val result = + tokens.zip(other.tokens).fold(0) { acc, (token, otherToken) -> + acc.takeIf { it != 0 } ?: token.compareTo(otherToken) + } + if (result != 0) result else tokens.size.compareTo(other.tokens.size) + } + is Unknown -> 1 + } + } + + /** + * A placeholder name that is used when a [Known] name could not be obtained for the item. + * + * @author Alexander Capehart + */ + data class Unknown(val placeholder: Placeholder) : Name { + override fun compareTo(other: Name) = + when (other) { + // Unknown names do not need any direct comparison right now. + is Unknown -> 0 + // Unknown names always come before known names. + is Known -> -1 + } + } +} + +/** An individual part of a name string that can be compared intelligently. */ +class Token internal constructor(internal val collationKey: CollationKey, internal val type: Type) : + Comparable { + override fun equals(other: Any?) = + other is Token && collationKey == other.collationKey && type == other.type + + override fun hashCode() = 31 * collationKey.hashCode() + type.hashCode() + + val value: String + get() = collationKey.sourceString + + override fun compareTo(other: Token): Int { + // Numeric tokens should always be lower than lexicographic tokens. + val modeComp = type.compareTo(other.type) + if (modeComp != 0) { + return modeComp + } + + // Numeric strings must be ordered by magnitude, thus immediately short-circuit + // the comparison if the lengths do not match. + if (type == Type.NUMERIC && + collationKey.sourceString.length != other.collationKey.sourceString.length) { + return collationKey.sourceString.length - other.collationKey.sourceString.length + } + + return collationKey.compareTo(other.collationKey) + } + + /** Denotes the type of comparison to be performed with this token. */ + internal enum class Type { + /** Compare as a digit string, like "65". */ + NUMERIC, + /** Compare as a standard alphanumeric string, like "65daysofstatic" */ + LEXICOGRAPHIC + } +} + +enum class Placeholder { + ALBUM, + ARTIST, + GENRE +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/ReleaseType.kt similarity index 77% rename from app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/ReleaseType.kt index 3fe45b202..709f71588 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/ReleaseType.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/ReleaseType.kt @@ -16,10 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info - -import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.info.ReleaseType.Album +package org.oxycblt.musikr.tag /** * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc. @@ -36,25 +33,13 @@ sealed interface ReleaseType { */ val refinement: Refinement? - /** The string resource corresponding to the name of this release type to show in the UI. */ - val stringRes: Int - /** * A plain album. * * @param refinement A specification of what kind of performance this release is. If null, the * release is considered "Plain". */ - data class Album(override val refinement: Refinement?) : ReleaseType { - override val stringRes: Int - get() = - when (refinement) { - null -> R.string.lbl_album - // If present, include the refinement in the name of this release type. - Refinement.LIVE -> R.string.lbl_album_live - Refinement.REMIX -> R.string.lbl_album_remix - } - } + data class Album(override val refinement: Refinement?) : ReleaseType /** * A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs. @@ -62,16 +47,7 @@ sealed interface ReleaseType { * @param refinement A specification of what kind of performance this release is. If null, the * release is considered "Plain". */ - data class EP(override val refinement: Refinement?) : ReleaseType { - override val stringRes: Int - get() = - when (refinement) { - null -> R.string.lbl_ep - // If present, include the refinement in the name of this release type. - Refinement.LIVE -> R.string.lbl_ep_live - Refinement.REMIX -> R.string.lbl_ep_remix - } - } + data class EP(override val refinement: Refinement?) : ReleaseType /** * A single. Usually a release consisting of 1-2 songs. @@ -79,16 +55,7 @@ sealed interface ReleaseType { * @param refinement A specification of what kind of performance this release is. If null, the * release is considered "Plain". */ - data class Single(override val refinement: Refinement?) : ReleaseType { - override val stringRes: Int - get() = - when (refinement) { - null -> R.string.lbl_single - // If present, include the refinement in the name of this release type. - Refinement.LIVE -> R.string.lbl_single_live - Refinement.REMIX -> R.string.lbl_single_remix - } - } + data class Single(override val refinement: Refinement?) : ReleaseType /** * A compilation. Usually consists of many songs from a variety of artists. @@ -96,16 +63,7 @@ sealed interface ReleaseType { * @param refinement A specification of what kind of performance this release is. If null, the * release is considered "Plain". */ - data class Compilation(override val refinement: Refinement?) : ReleaseType { - override val stringRes: Int - get() = - when (refinement) { - null -> R.string.lbl_compilation - // If present, include the refinement in the name of this release type. - Refinement.LIVE -> R.string.lbl_compilation_live - Refinement.REMIX -> R.string.lbl_compilation_remix - } - } + data class Compilation(override val refinement: Refinement?) : ReleaseType /** * A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually @@ -114,9 +72,6 @@ sealed interface ReleaseType { data object Soundtrack : ReleaseType { override val refinement: Refinement? get() = null - - override val stringRes: Int - get() = R.string.lbl_soundtrack } /** @@ -126,9 +81,6 @@ sealed interface ReleaseType { data object Mix : ReleaseType { override val refinement: Refinement? get() = null - - override val stringRes: Int - get() = R.string.lbl_mix } /** @@ -138,9 +90,6 @@ sealed interface ReleaseType { data object Mixtape : ReleaseType { override val refinement: Refinement? get() = null - - override val stringRes: Int - get() = R.string.lbl_mixtape } /** @@ -150,9 +99,6 @@ sealed interface ReleaseType { data object Demo : ReleaseType { override val refinement: Refinement? get() = null - - override val stringRes: Int - get() = R.string.lbl_demo } /** A specification of what kind of performance a particular release is. */ diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/ReplayGainAdjustment.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/ReplayGainAdjustment.kt new file mode 100644 index 000000000..e429ab674 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/ReplayGainAdjustment.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 Auxio Project + * ReplayGainAdjustment.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag + +/** + * Represents a ReplayGain adjustment to apply during song playback. + * + * @param track The track-specific adjustment that should be applied. Null if not available. + * @param album A more general album-specific adjustment that should be applied. Null if not + * available. + */ +data class ReplayGainAdjustment(val track: Float?, val album: Float?) diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt similarity index 67% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt index f7465c73c..4e9390711 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2022 Auxio Project - * TagUtil.kt is part of Auxio. + * Copyright (c) 2024 Auxio Project + * ID3.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,125 +16,10 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata - -import org.oxycblt.auxio.util.positiveOrNull - -/// --- GENERIC PARSING --- - -// TODO: Remove the escaping checks, it's too expensive to do this for every single tag. - -// TODO: I want to eventually be able to move a lot of this into TagWorker once I no longer have -// to deal with the cross-module dependencies of MediaStoreExtractor. - -/** - * Split a [String] by the given selector, automatically handling escaped characters that satisfy - * the selector. - * - * @param selector A block that determines if the string should be split at a given character. - * @return One or more [String]s split by the selector. - */ -inline fun String.splitEscaped(selector: (Char) -> Boolean): List { - val split = mutableListOf() - var currentString = "" - var i = 0 - - while (i < length) { - val a = get(i) - val b = getOrNull(i + 1) - - if (selector(a)) { - // Non-escaped separator, split the string here, making sure any stray whitespace - // is removed. - split.add(currentString) - currentString = "" - i++ - continue - } - - if (b != null && a == '\\' && selector(b)) { - // Is an escaped character, add the non-escaped variant and skip two - // characters to move on to the next one. - currentString += b - i += 2 - } else { - // Non-escaped, increment normally. - currentString += a - i++ - } - } - - if (currentString.isNotEmpty()) { - // Had an in-progress split string that is now terminated, add it. - split.add(currentString) - } - - return split -} - -/** - * Fix trailing whitespace or blank contents in a [String]. - * - * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or - * empty. - */ -fun String.correctWhitespace() = trim().ifBlank { null } - -/** - * Fix trailing whitespace or blank contents within a list of [String]s. - * - * @return A list of non-blank strings with trailing whitespace removed. - */ -fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } +package org.oxycblt.musikr.tag.format /// --- ID3v2 PARSING --- -/** - * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an - * (optional) total value delimited by a /. - * - * @return The position value extracted from the string field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - * - * @see transformPositionField - */ -fun String.parseId3v2PositionField() = - split('/', limit = 2).let { - transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull()) - } - -/** - * Parse a vorbis-style position + total field. These fields consist of two fields for the position - * and total numbers. - * - * @param pos The position value, or null if not present. - * @param total The total value, if not present. - * @return The position value extracted from the field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - * - * @see transformPositionField - */ -fun parseVorbisPositionField(pos: String?, total: String?) = - transformPositionField(pos?.toIntOrNull(), total?.toIntOrNull()) - -/** - * Transform a raw position + total field into a position a way that tolerates placeholder values. - * - * @param pos The position value, or null if not present. - * @param total The total value, if not present. - * @return The position value extracted from the field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - */ -fun transformPositionField(pos: Int?, total: Int?) = - if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) { - pos - } else { - null - } - /** * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * representations of genre fields into their named counterparts, and split up singular ID3v2-style @@ -143,7 +28,7 @@ fun transformPositionField(pos: Int?, total: Int?) = * @return A list of one or more genre names, or null if this multi-value list has no valid * formatting. */ -fun List.parseId3GenreNames() = +internal fun List.parseId3GenreNames() = if (size == 1) { first().parseId3MultiValueGenre() } else { @@ -170,8 +55,8 @@ private fun String.parseId3v1Genre(): String? { // try to index the genre table with such. val numeric = toIntOrNull() - // Not a numeric value, try some other fixed values. - ?: return when (this) { + // Not a numeric value, try some other fixed values. + ?: return when (this) { // CR and RX are not technically ID3v1, but are formatted similarly to a plain // number. "CR" -> "Cover" diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt new file mode 100644 index 000000000..f351150bf --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Auxio Project + * Vorbis.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.format + +import org.oxycblt.musikr.util.positiveOrNull + +/** + * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an + * (optional) total value delimited by a /. + * + * @return The position value extracted from the string field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + * + * @see transformPositionField + */ +internal fun String.parseSlashPositionField() = + split('/', limit = 2).let { + transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull()) + } + +/** + * Parse a vorbis-style position + total field. These fields consist of two fields for the position + * and total numbers. + * + * @param pos The position value, or null if not present. + * @param total The total value, if not present. + * @return The position value extracted from the field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + * + * @see transformPositionField + */ +internal fun parseXiphPositionField(pos: String?, total: String?) = + pos?.let { posStr -> + posStr.toIntOrNull()?.let { transformPositionField(it, total?.toIntOrNull()) } + ?: posStr.parseSlashPositionField() + } + +/** + * Transform a raw position + total field into a position a way that tolerates placeholder values. + * + * @param pos The position value, or null if not present. + * @param total The total value, if not present. + * @return The position value extracted from the field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + */ +internal fun transformPositionField(pos: Int?, total: Int?) = + if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) { + pos + } else { + null + } diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt new file mode 100644 index 000000000..d1d8d0b63 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Naming.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Auxio Project + * Naming.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.interpret + +import java.text.CollationKey +import java.text.Collator +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.Placeholder +import org.oxycblt.musikr.tag.Token + +abstract class Naming { + fun name(raw: String?, sort: String?, placeholder: Placeholder): Name = + if (raw != null) { + name(raw, sort) + } else { + Name.Unknown(placeholder) + } + + abstract fun name(raw: String, sort: String?): Name.Known + + companion object { + fun intelligent(): Naming = IntelligentNaming + + fun simple(): Naming = SimpleNaming + } +} + +private data object IntelligentNaming : Naming() { + override fun name(raw: String, sort: String?) = IntelligentKnownName(raw, sort) +} + +private data object SimpleNaming : Naming() { + override fun name(raw: String, sort: String?) = SimpleKnownName(raw, sort) +} + +private val collator: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } +private val punctRegex by lazy { Regex("[\\p{Punct}+]") } + +// TODO: Consider how you want to handle whitespace and "gaps" in names. + +/** + * Plain [Name.Known] implementation that is internationalization-safe. + * + * @author Alexander Capehart (OxygenCobalt) + */ +private data class SimpleKnownName(override val raw: String, override val sort: String?) : + Name.Known() { + override val tokens = listOf(parseToken(sort ?: raw)) + + private fun parseToken(name: String): Token { + // Remove excess punctuation from the string, as those usually aren't considered in sorting. + val stripped = name.replace(punctRegex, "").trim().ifEmpty { name } + val collationKey = collator.getCollationKey(stripped) + // Always use lexicographic mode since we aren't parsing any numeric components + return Token(collationKey, Token.Type.LEXICOGRAPHIC) + } +} + +/** + * [Name.Known] implementation that adds advanced sorting behavior at the cost of localization. + * + * @author Alexander Capehart (OxygenCobalt) + */ +private data class IntelligentKnownName(override val raw: String, override val sort: String?) : + Name.Known() { + override val tokens = parseTokens(sort ?: raw) + + private fun parseTokens(name: String): List { + // TODO: This routine is consuming much of the song building runtime, find a way to + // optimize it + val stripped = + name + // Remove excess punctuation from the string, as those usually aren't + // considered in sorting. + .replace(punctRegex, "") + .ifEmpty { name } + .run { + // Strip any english articles like "the" or "an" from the start, as music + // sorting should ignore such when possible. + when { + length > 4 && startsWith("the ", ignoreCase = true) -> substring(4) + length > 3 && startsWith("an ", ignoreCase = true) -> substring(3) + length > 2 && startsWith("a ", ignoreCase = true) -> substring(2) + else -> this + } + } + + // To properly compare numeric components in names, we have to split them up into + // individual lexicographic and numeric tokens and then individually compare them + // with special logic. + return TOKEN_REGEX.findAll(stripped).mapTo(mutableListOf()) { match -> + // Remove excess whitespace where possible + val token = match.value.trim().ifEmpty { match.value } + val collationKey: CollationKey + val type: Token.Type + // Separate each token into their numeric and lexicographic counterparts. + if (token.first().isDigit()) { + // The digit string comparison breaks with preceding zero digits, remove those + val digits = + token.trimStart { Character.getNumericValue(it) == 0 }.ifEmpty { token } + // Other languages have other types of digit strings, still use collation keys + collationKey = collator.getCollationKey(digits) + type = Token.Type.NUMERIC + } else { + collationKey = collator.getCollationKey(token) + type = Token.Type.LEXICOGRAPHIC + } + Token(collationKey, type) + } + } + + companion object { + private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt new file mode 100644 index 000000000..4d8831acf --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Auxio Project + * PreMusic.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.interpret + +import android.net.Uri +import java.util.UUID +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.fs.Format +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.Disc +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.ReleaseType +import org.oxycblt.musikr.tag.ReplayGainAdjustment +import org.oxycblt.musikr.util.update + +internal data class PreSong( + val musicBrainzId: UUID?, + val name: Name.Known, + val rawName: String, + val track: Int?, + val disc: Disc?, + val date: Date?, + val uri: Uri, + val path: Path, + val format: Format, + val size: Long, + val durationMs: Long, + val bitrateKbps: Int, + val sampleRateHz: Int, + val replayGainAdjustment: ReplayGainAdjustment, + val modifiedMs: Long, + val addedMs: Long, + val cover: Cover?, + val preAlbum: PreAlbum, + val preArtists: List, + val preGenres: List +) { + val uid = + musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) } + ?: Music.UID.auxio(Music.UID.Item.SONG) { + // Song UIDs are based on the raw data without parsing so that they remain + // consistent across music setting changes. Parents are not held up to the + // same standard since grouping is already inherently linked to settings. + update(rawName) + update(preAlbum.rawName) + update(date) + + update(track) + update(disc?.number) + + update(preArtists.map { artist -> artist.rawName }) + update(preAlbum.preArtists.map { artist -> artist.rawName }) + } +} + +internal data class PreAlbum( + val musicBrainzId: UUID?, + val name: Name, + val rawName: String?, + val releaseType: ReleaseType, + val preArtists: List +) + +internal data class PreArtist(val musicBrainzId: UUID?, val name: Name, val rawName: String?) + +internal data class PreGenre( + val name: Name, + val rawName: String?, +) diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Separators.kt similarity index 93% rename from app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Separators.kt index 678e1ef2f..fe3eb4282 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/Separators.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Separators.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Auxio Project + * Copyright (c) 2024 Auxio Project * Separators.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify @@ -16,7 +16,10 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.musikr.tag.interpret + +import org.oxycblt.musikr.util.correctWhitespace +import org.oxycblt.musikr.util.splitEscaped /** * Defines the user-specified parsing of multi-value tags. This should be used to parse any tags diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt new file mode 100644 index 000000000..1deb269aa --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagInterpreter.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.interpret + +import org.oxycblt.musikr.Interpretation +import org.oxycblt.musikr.fs.Format +import org.oxycblt.musikr.pipeline.RawSong +import org.oxycblt.musikr.tag.Disc +import org.oxycblt.musikr.tag.Name +import org.oxycblt.musikr.tag.Placeholder +import org.oxycblt.musikr.tag.ReleaseType +import org.oxycblt.musikr.tag.ReplayGainAdjustment +import org.oxycblt.musikr.tag.format.parseId3GenreNames +import org.oxycblt.musikr.tag.parse.ParsedTags +import org.oxycblt.musikr.util.toUuidOrNull + +internal interface TagInterpreter { + fun interpret(song: RawSong): PreSong + + companion object { + fun new(interpretation: Interpretation): TagInterpreter = TagInterpreterImpl(interpretation) + } +} + +private class TagInterpreterImpl(private val interpretation: Interpretation) : TagInterpreter { + override fun interpret(song: RawSong): PreSong { + val individualPreArtists = + makePreArtists( + song.tags.artistMusicBrainzIds, + song.tags.artistNames, + song.tags.artistSortNames, + interpretation) + val albumPreArtists = + makePreArtists( + song.tags.albumArtistMusicBrainzIds, + song.tags.albumArtistNames, + song.tags.albumArtistSortNames, + interpretation) + val preAlbum = + makePreAlbum(song.tags, individualPreArtists, albumPreArtists, interpretation) + val rawArtists = + individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } + val rawGenres = + makePreGenres(song.tags, interpretation).ifEmpty { listOf(unknownPreGenre()) } + val uri = song.file.uri + return PreSong( + uri = uri, + path = song.file.path, + size = song.file.size, + format = Format.infer(song.file.mimeType, song.properties.mimeType), + modifiedMs = song.file.modifiedMs, + addedMs = song.addedMs, + musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), + name = interpretation.naming.name(song.tags.name, song.tags.sortName), + rawName = song.tags.name, + track = song.tags.track, + disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) }, + date = song.tags.date, + durationMs = song.tags.durationMs, + bitrateKbps = song.properties.bitrateKbps, + sampleRateHz = song.properties.sampleRateHz, + replayGainAdjustment = + ReplayGainAdjustment( + song.tags.replayGainTrackAdjustment, + song.tags.replayGainAlbumAdjustment, + ), + preAlbum = preAlbum, + preArtists = rawArtists, + preGenres = rawGenres, + cover = song.cover) + } + + private fun makePreAlbum( + parsedTags: ParsedTags, + individualPreArtists: List, + albumPreArtists: List, + interpretation: Interpretation + ): PreAlbum { + return PreAlbum( + musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(), + name = + interpretation.naming.name( + parsedTags.albumName, parsedTags.albumSortName, Placeholder.ALBUM), + rawName = parsedTags.albumName, + releaseType = + ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes)) + ?: ReleaseType.Album(null), + preArtists = + albumPreArtists + .ifEmpty { individualPreArtists } + .ifEmpty { listOf(unknownPreArtist()) }) + } + + private fun makePreArtists( + rawMusicBrainzIds: List, + rawNames: List, + rawSortNames: List, + interpretation: Interpretation + ): List { + val musicBrainzIds = interpretation.separators.split(rawMusicBrainzIds) + val names = interpretation.separators.split(rawNames) + val sortNames = interpretation.separators.split(rawSortNames) + return names.mapIndexed { i, name -> + makePreArtist(musicBrainzIds.getOrNull(i), name, sortNames.getOrNull(i), interpretation) + } + } + + private fun makePreArtist( + musicBrainzId: String?, + rawName: String?, + sortName: String?, + interpretation: Interpretation + ): PreArtist { + val name = interpretation.naming.name(rawName, sortName, Placeholder.ARTIST) + val musicBrainzId = musicBrainzId?.toUuidOrNull() + return PreArtist(musicBrainzId, name, rawName) + } + + private fun unknownPreArtist() = PreArtist(null, Name.Unknown(Placeholder.ARTIST), null) + + private fun makePreGenres( + parsedTags: ParsedTags, + interpretation: Interpretation + ): List { + val genreNames = + parsedTags.genreNames.parseId3GenreNames() + ?: interpretation.separators.split(parsedTags.genreNames) + return genreNames.map { makePreGenre(it, interpretation) } + } + + private fun makePreGenre(rawName: String?, interpretation: Interpretation) = + PreGenre(interpretation.naming.name(rawName, null, Placeholder.GENRE), rawName) + + private fun unknownPreGenre() = PreGenre(Name.Unknown(Placeholder.GENRE), null) +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt new file mode 100644 index 000000000..a7dc4d3c5 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Auxio Project + * ParsedTags.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.parse + +import org.oxycblt.musikr.tag.Date + +internal data class ParsedTags( + val durationMs: Long, + val replayGainTrackAdjustment: Float? = null, + val replayGainAlbumAdjustment: Float? = null, + val musicBrainzId: String? = null, + val name: String, + val sortName: String? = null, + val track: Int? = null, + val disc: Int? = null, + val subtitle: String? = null, + val date: Date? = null, + val albumMusicBrainzId: String? = null, + val albumName: String? = null, + val albumSortName: String? = null, + val releaseTypes: List = listOf(), + val artistMusicBrainzIds: List = listOf(), + val artistNames: List = listOf(), + val artistSortNames: List = listOf(), + val albumArtistMusicBrainzIds: List = listOf(), + val albumArtistNames: List = listOf(), + val albumArtistSortNames: List = listOf(), + val genreNames: List = listOf() +) diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt new file mode 100644 index 000000000..08620d399 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagFields.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.parse + +import androidx.core.text.isDigitsOnly +import org.oxycblt.musikr.metadata.Metadata +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.format.parseSlashPositionField +import org.oxycblt.musikr.tag.format.parseXiphPositionField +import org.oxycblt.musikr.util.nonZeroOrNull + +// Note: TagLibJNI deliberately uppercases descriptive tags to avoid casing issues, +// hence why the casing here is matched. Note that MP4 atoms are kept in their +// original casing, as they are case-sensitive. + +// Song +internal fun Metadata.musicBrainzId() = + (xiph["MUSICBRAINZ_RELEASETRACKID"] + ?: xiph["MUSICBRAINZ RELEASE TRACK ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ RELEASE TRACK ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_RELEASETRACKID"] + ?: id3v2["TXXX:MUSICBRAINZ RELEASE TRACK ID"] + ?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"]) + ?.first() + +internal fun Metadata.name() = + (xiph["TITLE"] ?: mp4["©nam"] ?: mp4["©trk"] ?: id3v2["TIT2"])?.first() + +internal fun Metadata.sortName() = (xiph["TITLESORT"] ?: mp4["sonm"] ?: id3v2["TSOT"])?.first() + +// Track. +internal fun Metadata.track() = + (parseXiphPositionField( + xiph["TRACKNUMBER"]?.first(), + (xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first()) + ?: (mp4["trkn"] ?: id3v2["TRCK"])?.run { first().parseSlashPositionField() }) + +// Disc and it's subtitle name. +internal fun Metadata.disc() = + (parseXiphPositionField( + xiph["DISCNUMBER"]?.first(), + (xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() }) + ?: (mp4["disk"] ?: id3v2["TPOS"])?.run { first().parseSlashPositionField() }) + +internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first() + +internal fun Metadata.date() = + // For ID3v 2, are somewhat complicated, as not only did their semantics change from a flat + // year value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of + // date types. + // Our hierarchy for dates is as such: + // 1. ID3v2.4 Original Date, as it resolves the "Released in X, Remastered in Y" issue + // 2. ID3v2.4 Recording Date, as it is the most common date type + // 3. ID3v2.4 Release Date, as it is the second most common date type + // 4. ID3v2.3 Original Date, as it is like #1 + // 5. ID3v2.3 Release Year, as it is the most common date type + // xiph dates aren't complicated, but there are still several types + // Our hierarchy for dates is as such: + // 1. Original Date, as it solves the "Released in X, Remastered in Y" issue + // 2. Date, as it is the most common date type + // 3. Year, as old xiph tags tended to use this (I know this because it's the only + // date tag that android supports, so it must be 15 years old or more!) + // TODO: Show original and normal dates side-by-side + // TODO: Handle dates that are in "January" because the actual specific release date + // isn't known? + ((xiph["ORIGINALDATE"] + ?: xiph["DATE"] + ?: xiph["YEAR"] + ?: mp4["©day"] + ?: id3v2["TDOR"] + ?: id3v2["TDRC"] + ?: id3v2["TDRL"]) + ?.run { Date.from(first()) } ?: parseId3v23Date()) + +// Album +internal fun Metadata.albumMusicBrainzId() = + (xiph["MUSICBRAINZ_ALBUMID"] + ?: xiph["MUSICBRAINZ ALBUM ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ALBUMID"] + ?: id3v2["TXXX:MUSICBRAINZ ALBUM ID"] + ?: id3v2["TXXX:MUSICBRAINZ_ALBUMID"]) + ?.first() + +internal fun Metadata.albumName() = (xiph["ALBUM"] ?: mp4["©alb"] ?: id3v2["TALB"])?.first() + +internal fun Metadata.albumSortName() = (xiph["ALBUMSORT"] ?: mp4["soal"] ?: id3v2["TSOA"])?.first() + +internal fun Metadata.releaseTypes() = + // GRP/GRP1 are assumed to also pertain to release types. + // GRP1 is a non-standard iTunes extension. + (xiph["RELEASETYPE"] + ?: xiph["MUSICBRAINZ ALBUM TYPE"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM TYPE"] + ?: mp4["----:COM.APPLE.ITUNES:RELEASETYPE"] + ?: mp4["©grp"] + ?: id3v2["TXXX:MUSICBRAINZ ALBUM TYPE"] + ?: id3v2["TXXX:RELEASETYPE"] + ?: id3v2["GRP1"]) + +// Artist +internal fun Metadata.artistMusicBrainzIds() = + (xiph["MUSICBRAINZ_ARTISTID"] + ?: xiph["MUSICBRAINZ ARTIST ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ARTIST ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ARTISTID"] + ?: id3v2["TXXX:MUSICBRAINZ ARTIST ID"] + ?: id3v2["TXXX:MUSICBRAINZ_ARTISTID"]) + +internal fun Metadata.artistNames() = + (xiph["ARTISTS"] + ?: xiph["ARTIST"] + ?: mp4["----:COM.APPLE.ITUNES:ARTISTS"] + ?: mp4["©ART"] + ?: mp4["----:COM.APPLE.ITUNES:ARTIST"] + ?: id3v2["TXXX:ARTISTS"] + ?: id3v2["TPE1"] + ?: id3v2["TXXX:ARTIST"]) + +internal fun Metadata.artistSortNames() = + (xiph["ARTISTSSORT"] + ?: xiph["ARTISTS_SORT"] + ?: xiph["ARTISTS SORT"] + ?: xiph["ARTISTSORT"] + ?: xiph["ARTIST SORT"] + ?: mp4["----:COM.APPLE.ITUNES:ARTISTSSORT"] + ?: mp4["----:COM.APPLE.ITUNES:ARTISTS_SORT"] + ?: mp4["----:COM.APPLE.ITUNES:ARTISTS SORT"] + ?: mp4["soar"] + ?: mp4["----:COM.APPLE.ITUNES:ARTISTSORT"] + ?: mp4["----:COM.APPLE.ITUNES:ARTIST SORT"] + ?: id3v2["TXXX:ARTISTSSORT"] + ?: id3v2["TXXX:ARTISTS_SORT"] + ?: id3v2["TXXX:ARTISTS SORT"] + ?: id3v2["TSOP"] + ?: id3v2["TXXX:ARTISTSORT"] + ?: id3v2["TXXX:ARTIST SORT"]) + +internal fun Metadata.albumArtistMusicBrainzIds() = + (xiph["MUSICBRAINZ_ALBUMARTISTID"] + ?: xiph["MUSICBRAINZ ALBUM ARTIST ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM ARTIST ID"] + ?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ALBUMARTISTID"] + ?: id3v2["TXXX:MUSICBRAINZ ALBUM ARTIST ID"] + ?: id3v2["TXXX:MUSICBRAINZ_ALBUMARTISTID"]) + +internal fun Metadata.albumArtistNames() = + (xiph["ALBUMARTISTS"] + ?: xiph["ALBUM_ARTISTS"] + ?: xiph["ALBUM ARTISTS"] + ?: xiph["ALBUMARTIST"] + ?: xiph["ALBUM ARTIST"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUM_ARTISTS"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTISTS"] + ?: mp4["aART"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTIST"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTIST"] + ?: id3v2["TXXX:ALBUMARTISTS"] + ?: id3v2["TXXX:ALBUM_ARTISTS"] + ?: id3v2["TXXX:ALBUM ARTISTS"] + ?: id3v2["TPE2"] + ?: id3v2["TXXX:ALBUMARTIST"] + ?: id3v2["TXXX:ALBUM ARTIST"]) + +internal fun Metadata.albumArtistSortNames() = + // TSO2 is a non-standard iTunes extension. + (xiph["ALBUMARTISTSSORT"] + ?: xiph["ALBUMARTISTS_SORT"] + ?: xiph["ALBUMARTISTS SORT"] + ?: xiph["ALBUMARTISTSORT"] + ?: xiph["ALBUM ARTIST SORT"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTSSORT"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS_SORT"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS SORT"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTSORT"] + ?: mp4["soaa"] + ?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTIST SORT"] + ?: id3v2["TXXX:ALBUMARTISTSSORT"] + ?: id3v2["TXXX:ALBUMARTISTS_SORT"] + ?: id3v2["TXXX:ALBUMARTISTS SORT"] + ?: id3v2["TXXX:ALBUMARTISTSORT"] + ?: id3v2["TSO2"] + ?: id3v2["TXXX:ALBUM ARTIST SORT"]) + +// Genre +internal fun Metadata.genreNames() = xiph["GENRE"] ?: mp4["©gen"] ?: mp4["gnre"] ?: id3v2["TCON"] + +// Compilation Flag +internal fun Metadata.isCompilation() = + // TCMP is a non-standard itunes extension + // We also only look for tags that are actually valid + // (i.e. 1 for true, 0 for false) + (xiph["COMPILATION"] + ?: xiph["ITUNESCOMPILATION"] + ?: mp4["cpil"] + ?: mp4["----:COM.APPLE.ITUNES:COMPILATION"] + ?: mp4["----:COM.APPLE.ITUNES:ITUNESCOMPILATION"] + ?: id3v2["TCMP"] + ?: id3v2["TXXX:COMPILATION"] + ?: id3v2["TXXX:ITUNESCOMPILATION"]) == listOf("1") + +// ReplayGain information +internal fun Metadata.replayGainTrackAdjustment() = + (xiph["R128_TRACK_GAIN"]?.parseR128Adjustment() + ?: xiph["REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment() + ?: mp4["----:COM.APPLE.ITUNES:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment() + ?: id3v2["TXXX:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()) + +internal fun Metadata.replayGainAlbumAdjustment() = + (xiph["R128_ALBUM_GAIN"]?.parseR128Adjustment() + ?: xiph["REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment() + ?: mp4["----:COM.APPLE.ITUNES:REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment() + ?: id3v2["TXXX:REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment()) + +private fun List.parseR128Adjustment() = + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull()?.run { + // Convert to fixed-point and adjust to LUFS 18 to match the ReplayGain scale + this / 256f + 5 + } + +/** + * Parse a ReplayGain adjustment into a float value. + * + * @return A parsed adjustment float, or null if the adjustment had invalid formatting. + */ +private fun List.parseReplayGainAdjustment() = + first().replace(REPLAYGAIN_ADJUSTMENT_FILTER_REGEX, "").toFloatOrNull()?.nonZeroOrNull() + +/** + * Matches non-float information from ReplayGain adjustments. Derived from vanilla music: + * https://github.com/vanilla-music/vanilla + */ +private val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } + +private fun Metadata.parseId3v23Date(): Date? { + // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY + // is present. + val year = + id3v2["TORY"]?.run { first().toIntOrNull() } + ?: id3v2["TYER"]?.run { first().toIntOrNull() } + ?: return null + + val tdat = id3v2["TDAT"] + return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { + // TDAT frames consist of a 4-digit string where the first two digits are + // the month and the last two digits are the day. + val mm = tdat.first().substring(0..1).toInt() + val dd = tdat.first().substring(2..3).toInt() + + val time = id3v2["TIME"] + if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) { + // TIME frames consist of a 4-digit string where the first two digits are + // the hour and the last two digits are the minutes. No second value is + // possible. + val hh = time.first().substring(0..1).toInt() + val mi = time.first().substring(2..3).toInt() + // Able to return a full date. + Date.from(year, mm, dd, hh, mi) + } else { + // Unable to parse time, just return a date + Date.from(year, mm, dd) + } + } else { + // Unable to parse month/day, just return a year + return Date.from(year) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt new file mode 100644 index 000000000..42d76af43 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Auxio Project + * TagParser.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.tag.parse + +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.metadata.Metadata +import org.oxycblt.musikr.util.unlikelyToBeNull + +internal interface TagParser { + fun parse(file: DeviceFile, metadata: Metadata): ParsedTags + + companion object { + fun new(): TagParser = TagParserImpl + } +} + +private data object TagParserImpl : TagParser { + override fun parse(file: DeviceFile, metadata: Metadata): ParsedTags { + val compilation = metadata.isCompilation() + return ParsedTags( + durationMs = metadata.properties.durationMs, + replayGainTrackAdjustment = metadata.replayGainTrackAdjustment(), + replayGainAlbumAdjustment = metadata.replayGainAlbumAdjustment(), + musicBrainzId = metadata.musicBrainzId(), + name = metadata.name() ?: unlikelyToBeNull(file.path.name), + sortName = metadata.sortName(), + track = metadata.track(), + disc = metadata.disc(), + subtitle = metadata.subtitle(), + date = metadata.date(), + albumMusicBrainzId = metadata.albumMusicBrainzId(), + albumName = metadata.albumName(), + albumSortName = metadata.albumSortName(), + // Compilation flag implies a compilation release type in the case that + // we don't have any other release types + releaseTypes = + metadata.releaseTypes() ?: listOf("compilation").takeIf { compilation } ?: listOf(), + artistMusicBrainzIds = metadata.artistMusicBrainzIds() ?: listOf(), + artistNames = metadata.artistNames() ?: listOf(), + artistSortNames = metadata.artistSortNames() ?: listOf(), + albumArtistMusicBrainzIds = metadata.albumArtistMusicBrainzIds() ?: listOf(), + // Compilation pretty heavily implies various artists in the case that we don't + // have any other album artists + albumArtistNames = + metadata.albumArtistNames() + ?: listOf("Various Artists").takeIf { compilation } + ?: listOf(), + albumArtistSortNames = metadata.albumArtistSortNames() ?: listOf(), + genreNames = metadata.genreNames() ?: listOf()) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/track/LocationObserver.kt b/musikr/src/main/java/org/oxycblt/musikr/track/LocationObserver.kt new file mode 100644 index 000000000..f5e9cbda8 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/track/LocationObserver.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Auxio Project + * LocationObserver.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.track + +import android.content.Context +import android.database.ContentObserver +import android.os.Handler +import android.os.Looper +import org.oxycblt.musikr.fs.MusicLocation + +internal class LocationObserver( + private val context: Context, + private val location: MusicLocation, + private val listener: UpdateTracker.Callback +) : ContentObserver(Handler(Looper.getMainLooper())), Runnable { + private val handler = Handler(Looper.getMainLooper()) + + init { + context.applicationContext.contentResolver.registerContentObserver(location.uri, true, this) + } + + fun release() { + handler.removeCallbacks(this) + context.applicationContext.contentResolver.unregisterContentObserver(this) + } + + override fun onChange(selfChange: Boolean) { + // Batch rapid-fire updates into a single callback after delay + handler.removeCallbacks(this) + handler.postDelayed(this, REINDEX_DELAY_MS) + } + + override fun run() { + listener.onUpdate(location) + } + + private companion object { + const val REINDEX_DELAY_MS = 500L + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/track/UpdateTracker.kt b/musikr/src/main/java/org/oxycblt/musikr/track/UpdateTracker.kt new file mode 100644 index 000000000..88a9dec98 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/track/UpdateTracker.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Auxio Project + * UpdateTracker.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.track + +import android.content.Context +import org.oxycblt.musikr.fs.MusicLocation + +interface UpdateTracker { + fun track(locations: List) + + fun release() + + interface Callback { + fun onUpdate(location: MusicLocation) + } + + companion object { + fun from(context: Context, callback: Callback): UpdateTracker = + UpdateTrackerImpl(context, callback) + } +} + +private class UpdateTrackerImpl( + private val context: Context, + private val callback: UpdateTracker.Callback +) : UpdateTracker { + private val observers = mutableListOf() + + override fun track(locations: List) { + release() + observers.addAll(locations.map { LocationObserver(context, it, callback) }) + } + + override fun release() { + observers.forEach { it.release() } + observers.clear() + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/util/LangUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/util/LangUtil.kt new file mode 100644 index 000000000..a1b361370 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/util/LangUtil.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021 Auxio Project + * LangUtil.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.util + +import java.security.MessageDigest +import java.util.UUID +import kotlin.reflect.KClass +import org.oxycblt.musikr.BuildConfig +import org.oxycblt.musikr.tag.Date + +/** + * Sanitizes a value that is unlikely to be null. On debug builds, this aliases to [requireNotNull], + * otherwise, it aliases to the unchecked dereference operator (!!). This can be used as a minor + * optimization in certain cases. + */ +internal fun unlikelyToBeNull(value: T?) = + if (BuildConfig.DEBUG) { + requireNotNull(value) + } else { + value!! + } + +/** + * Aliases a check to ensure that the given number is non-zero. + * + * @return The given number if it's non-zero, null otherwise. + */ +internal fun Int.positiveOrNull() = if (this > 0) this else null + +/** + * Aliases a check to ensure that the given number is non-zero. + * + * @return The same number if it's non-zero, null otherwise. + */ +internal fun Float.nonZeroOrNull() = if (this != 0f) this else null + +/** + * Aliases a check to ensure a given value is in a specified range. + * + * @param range The valid range of values for this number. + * @return The same number if it is in the range, null otherwise. + */ +internal fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null + +/** + * Convert a [String] to a [UUID]. + * + * @return A [UUID] converted from the [String] value, or null if the value was not valid. + * @see UUID.fromString + */ +internal fun String.toUuidOrNull(): UUID? = + try { + UUID.fromString(this) + } catch (e: IllegalArgumentException) { + null + } + +/** + * Update a [MessageDigest] with a lowercase [String]. + * + * @param string The [String] to hash. If null, it will not be hashed. + */ +internal fun MessageDigest.update(string: String?) { + if (string != null) { + update(string.lowercase().toByteArray()) + } else { + update(0) + } +} + +/** + * Update a [MessageDigest] with the string representation of a [Date]. + * + * @param date The [Date] to hash. If null, nothing will be done. + */ +internal fun MessageDigest.update(date: Date?) { + if (date != null) { + update(date.toString().toByteArray()) + } else { + update(0) + } +} + +/** + * Update a [MessageDigest] with the lowercase versions of all of the input [String]s. + * + * @param strings The [String]s to hash. If a [String] is null, it will not be hashed. + */ +internal fun MessageDigest.update(strings: List) { + strings.forEach(::update) +} + +/** + * Update a [MessageDigest] with the little-endian bytes of a [Int]. + * + * @param n The [Int] to write. If null, nothing will be done. + */ +internal fun MessageDigest.update(n: Int?) { + if (n != null) { + update(byteArrayOf(n.toByte(), n.shr(8).toByte(), n.shr(16).toByte(), n.shr(24).toByte())) + } else { + update(0) + } +} + +/** + * Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material + * Files: https://github.com/zhanghai/MaterialFiles + * + * @param clazz The [KClass] to reflect into. + * @param method The name of the method to obtain. + */ +internal fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = + lazy { + clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also { + it.isAccessible = true + } + } diff --git a/musikr/src/main/java/org/oxycblt/musikr/util/ParseUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/util/ParseUtil.kt new file mode 100644 index 000000000..d460d6455 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/util/ParseUtil.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 Auxio Project + * ParseUtil.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.musikr.util + +/** + * Split a [String] by the given selector, automatically handling escaped characters that satisfy + * the selector. + * + * @param selector A block that determines if the string should be split at a given character. + * @return One or more [String]s split by the selector. + */ +internal inline fun String.splitEscaped(selector: (Char) -> Boolean): List { + val split = mutableListOf() + var currentString = "" + var i = 0 + + while (i < length) { + val a = get(i) + val b = getOrNull(i + 1) + + if (selector(a)) { + // Non-escaped separator, split the string here, making sure any stray whitespace + // is removed. + split.add(currentString) + currentString = "" + i++ + continue + } + + if (b != null && a == '\\' && selector(b)) { + // Is an escaped character, add the non-escaped variant and skip two + // characters to move on to the next one. + currentString += b + i += 2 + } else { + // Non-escaped, increment normally. + currentString += a + i++ + } + } + + if (currentString.isNotEmpty()) { + // Had an in-progress split string that is now terminated, add it. + split.add(currentString) + } + + return split +} + +// TODO: Remove the escaping checks, it's too expensive to do this for every single tag. + +/** + * Fix trailing whitespace or blank contents in a [String]. + * + * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or + * empty. + */ +internal fun String.correctWhitespace() = trim().ifBlank { null } + +/** + * Fix trailing whitespace or blank contents within a list of [String]s. + * + * @return A list of non-blank strings with trailing whitespace removed. + */ +internal fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/DateTest.kt similarity index 98% rename from app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/DateTest.kt index b63639e27..b2cb3380d 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/DateTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/DateTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info +package org.oxycblt.musikr.tag import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/DiscTest.kt similarity index 97% rename from app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/DiscTest.kt index 9b428acac..ab79073f9 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/DiscTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/DiscTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info +package org.oxycblt.musikr.tag import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/NameTest.kt similarity index 50% rename from app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/NameTest.kt index 078a1f154..a44f79708 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/NameTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/NameTest.kt @@ -16,342 +16,321 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info +package org.oxycblt.musikr.tag import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Test +import org.oxycblt.musikr.tag.interpret.Naming class NameTest { @Test fun name_simple_withoutPunct() { - val name = Name.Known.SimpleFactory.parse("Loveless", null) + val name = Naming.simple().name("Loveless", null) assertEquals("Loveless", name.raw) assertEquals(null, name.sort) - assertEquals("L", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("Loveless", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_simple_withPunct() { - val name = Name.Known.SimpleFactory.parse("alt-J", null) + val name = Naming.simple().name("alt-J", null) assertEquals("alt-J", name.raw) assertEquals(null, name.sort) - assertEquals("A", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("altJ", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_simple_oopsAllPunct() { - val name = Name.Known.SimpleFactory.parse("!!!", null) + val name = Naming.simple().name("!!!", null) assertEquals("!!!", name.raw) assertEquals(null, name.sort) - assertEquals("!", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("!!!", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_simple_spacedPunct() { - val name = Name.Known.SimpleFactory.parse("& Yet & Yet", null) + val name = Naming.simple().name("& Yet & Yet", null) assertEquals("& Yet & Yet", name.raw) assertEquals(null, name.sort) - assertEquals("Y", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Yet Yet", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test fun name_simple_withSort() { - val name = Name.Known.SimpleFactory.parse("The Smile", "Smile") + val name = Naming.simple().name("The Smile", "Smile") assertEquals("The Smile", name.raw) assertEquals("Smile", name.sort) - assertEquals("S", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("Smile", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("Loveless", null) + val name = Naming.intelligent().name("Loveless", null) assertEquals("Loveless", name.raw) assertEquals(null, name.sort) - assertEquals("L", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("Loveless", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() { - val name = Name.Known.IntelligentFactory.parse("15 Step", null) + val name = Naming.intelligent().name("15 Step", null) assertEquals("15 Step", name.raw) assertEquals(null, name.sort) - assertEquals("#", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("15", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.NUMERIC, first.type) + val second = name.tokens[1] assertEquals("Step", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() { - val name = Name.Known.IntelligentFactory.parse("23Kid", null) + val name = Naming.intelligent().name("23Kid", null) assertEquals("23Kid", name.raw) assertEquals(null, name.sort) - assertEquals("#", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("23", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.NUMERIC, first.type) + val second = name.tokens[1] assertEquals("Kid", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() { - val name = Name.Known.IntelligentFactory.parse("Foo 1 2 Bar", null) + val name = Naming.intelligent().name("Foo 1 2 Bar", null) assertEquals("Foo 1 2 Bar", name.raw) assertEquals(null, name.sort) - assertEquals("F", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) + val second = name.tokens[1] assertEquals("1", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) - val third = name.sortTokens[2] + assertEquals(Token.Type.NUMERIC, second.type) + val third = name.tokens[2] assertEquals(" ", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) - val fourth = name.sortTokens[3] + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) + val fourth = name.tokens[3] assertEquals("2", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fourth.type) - val fifth = name.sortTokens[4] + assertEquals(Token.Type.NUMERIC, fourth.type) + val fifth = name.tokens[4] assertEquals("Bar", fifth.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, fifth.type) + assertEquals(Token.Type.LEXICOGRAPHIC, fifth.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() { - val name = Name.Known.IntelligentFactory.parse("Foo12Bar", null) + val name = Naming.intelligent().name("Foo12Bar", null) assertEquals("Foo12Bar", name.raw) assertEquals(null, name.sort) - assertEquals("F", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) + val second = name.tokens[1] assertEquals("12", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) - val third = name.sortTokens[2] + assertEquals(Token.Type.NUMERIC, second.type) + val third = name.tokens[2] assertEquals("Bar", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() { - val name = Name.Known.IntelligentFactory.parse("Foo 1", null) + val name = Naming.intelligent().name("Foo 1", null) assertEquals("Foo 1", name.raw) assertEquals(null, name.sort) - assertEquals("F", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Foo", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) + val second = name.tokens[1] assertEquals("1", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) } @Test fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() { - val name = Name.Known.IntelligentFactory.parse("Error404", null) + val name = Naming.intelligent().name("Error404", null) assertEquals("Error404", name.raw) assertEquals(null, name.sort) - assertEquals("E", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Error", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) + val second = name.tokens[1] assertEquals("404", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) + assertEquals(Token.Type.NUMERIC, second.type) } @Test fun name_intelligent_withoutPunct_withThe_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("The National Anthem", null) + val name = Naming.intelligent().name("The National Anthem", null) assertEquals("The National Anthem", name.raw) assertEquals(null, name.sort) - assertEquals("N", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("National Anthem", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test fun name_intelligent_withoutPunct_withAn_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("An Eagle in Your Mind", null) + val name = Naming.intelligent().name("An Eagle in Your Mind", null) assertEquals("An Eagle in Your Mind", name.raw) assertEquals(null, name.sort) - assertEquals("E", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Eagle in Your Mind", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test fun name_intelligent_withoutPunct_withA_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("A Song For Our Fathers", null) + val name = Naming.intelligent().name("A Song For Our Fathers", null) assertEquals("A Song For Our Fathers", name.raw) assertEquals(null, name.sort) - assertEquals("S", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Song For Our Fathers", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test fun name_intelligent_withPunct_withoutArticle_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("alt-J", null) + val name = Naming.intelligent().name("alt-J", null) assertEquals("alt-J", name.raw) assertEquals(null, name.sort) - assertEquals("A", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("altJ", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("!!!", null) + val name = Naming.intelligent().name("!!!", null) assertEquals("!!!", name.raw) assertEquals(null, name.sort) - assertEquals("!", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("!!!", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_intelligent_withoutPunct_shortArticle_withNumerics() { - val name = Name.Known.IntelligentFactory.parse("the 1", null) + val name = Naming.intelligent().name("the 1", null) assertEquals("the 1", name.raw) assertEquals(null, name.sort) - assertEquals("#", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("1", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) + assertEquals(Token.Type.NUMERIC, first.type) } @Test fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() { - val name = Name.Known.IntelligentFactory.parse("& Yet & Yet", null) + val name = Naming.intelligent().name("& Yet & Yet", null) assertEquals("& Yet & Yet", name.raw) assertEquals(null, name.sort) - assertEquals("Y", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Yet Yet", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) } @Test fun name_intelligent_withPunct_withoutArticle_withNumerics() { - val name = Name.Known.IntelligentFactory.parse("Design : 2 : 3", null) + val name = Naming.intelligent().name("Design : 2 : 3", null) assertEquals("Design : 2 : 3", name.raw) assertEquals(null, name.sort) - assertEquals("D", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("Design", first.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.LEXICOGRAPHIC, first.type) + val second = name.tokens[1] assertEquals("2", second.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, second.type) - val third = name.sortTokens[2] + assertEquals(Token.Type.NUMERIC, second.type) + val third = name.tokens[2] assertEquals(" ", third.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, third.type) - val fourth = name.sortTokens[3] + assertEquals(Token.Type.LEXICOGRAPHIC, third.type) + val fourth = name.tokens[3] assertEquals("3", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fourth.type) + assertEquals(Token.Type.NUMERIC, fourth.type) } @Test fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() { - val name = Name.Known.IntelligentFactory.parse("2 + 2 = 5", null) + val name = Naming.intelligent().name("2 + 2 = 5", null) assertEquals("2 + 2 = 5", name.raw) assertEquals(null, name.sort) - assertEquals("#", name.thumb) - val first = name.sortTokens[0] + val first = name.tokens[0] assertEquals("2", first.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, first.type) - val second = name.sortTokens[1] + assertEquals(Token.Type.NUMERIC, first.type) + val second = name.tokens[1] assertEquals(" ", second.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, second.type) - val third = name.sortTokens[2] + assertEquals(Token.Type.LEXICOGRAPHIC, second.type) + val third = name.tokens[2] assertEquals("2", third.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, third.type) - val fourth = name.sortTokens[3] + assertEquals(Token.Type.NUMERIC, third.type) + val fourth = name.tokens[3] assertEquals(" ", fourth.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, fourth.type) - val fifth = name.sortTokens[4] + assertEquals(Token.Type.LEXICOGRAPHIC, fourth.type) + val fifth = name.tokens[4] assertEquals("5", fifth.collationKey.sourceString) - assertEquals(SortToken.Type.NUMERIC, fifth.type) + assertEquals(Token.Type.NUMERIC, fifth.type) } @Test fun name_intelligent_withSort() { - val name = Name.Known.IntelligentFactory.parse("The Smile", "Smile") + val name = Naming.intelligent().name("The Smile", "Smile") assertEquals("The Smile", name.raw) assertEquals("Smile", name.sort) - assertEquals("S", name.thumb) - val only = name.sortTokens.single() + val only = name.tokens.single() assertEquals("Smile", only.collationKey.sourceString) - assertEquals(SortToken.Type.LEXICOGRAPHIC, only.type) + assertEquals(Token.Type.LEXICOGRAPHIC, only.type) } @Test fun name_equals_simple() { - val a = Name.Known.SimpleFactory.parse("The Same", "Same") - val b = Name.Known.SimpleFactory.parse("The Same", "Same") + val a = Naming.simple().name("The Same", "Same") + val b = Naming.simple().name("The Same", "Same") assertEquals(a, b) } @Test fun name_equals_differentSort() { - val a = Name.Known.SimpleFactory.parse("The Same", "Same") - val b = Name.Known.SimpleFactory.parse("The Same", null) + val a = Naming.simple().name("The Same", "Same") + val b = Naming.simple().name("The Same", null) assertNotEquals(a, b) assertNotEquals(a.hashCode(), b.hashCode()) } @Test fun name_equals_intelligent_differentTokens() { - val a = Name.Known.IntelligentFactory.parse("The Same", "Same") - val b = Name.Known.IntelligentFactory.parse("Same", "Same") + val a = Naming.intelligent().name("The Same", "Same") + val b = Naming.intelligent().name("Same", "Same") assertNotEquals(a, b) assertNotEquals(a.hashCode(), b.hashCode()) } @Test fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() { - val a = Name.Known.SimpleFactory.parse("A", null) - val b = Name.Known.SimpleFactory.parse("B", null) + val a = Naming.simple().name("A", null) + val b = Naming.simple().name("B", null) assertEquals(-1, a.compareTo(b)) } @Test fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() { - val a = Name.Known.SimpleFactory.parse("A Brain in a Bottle", null) - val b = Name.Known.SimpleFactory.parse("Acid Rain", null) - val c = Name.Known.SimpleFactory.parse("Boralis / Contrastellar", null) - val d = Name.Known.SimpleFactory.parse("Breathe In", null) + val a = Naming.simple().name("A Brain in a Bottle", null) + val b = Naming.simple().name("Acid Rain", null) + val c = Naming.simple().name("Boralis / Contrastellar", null) + val d = Naming.simple().name("Breathe In", null) assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(c)) assertEquals(-1, a.compareTo(d)) @@ -359,40 +338,40 @@ class NameTest { @Test fun name_compareTo_simple_withSort_withoutArticle_withNumeric() { - val a = Name.Known.SimpleFactory.parse("15 Step", null) - val b = Name.Known.SimpleFactory.parse("128 Harps", null) - val c = Name.Known.SimpleFactory.parse("1969", null) + val a = Naming.simple().name("15 Step", null) + val b = Naming.simple().name("128 Harps", null) + val c = Naming.simple().name("1969", null) assertEquals(1, a.compareTo(b)) assertEquals(-1, a.compareTo(c)) } @Test fun name_compareTo_simple_withPartialSort() { - val a = Name.Known.SimpleFactory.parse("A", "C") - val b = Name.Known.SimpleFactory.parse("B", null) + val a = Naming.simple().name("A", "C") + val b = Naming.simple().name("B", null) assertEquals(1, a.compareTo(b)) } @Test fun name_compareTo_simple_withSort() { - val a = Name.Known.SimpleFactory.parse("D", "A") - val b = Name.Known.SimpleFactory.parse("C", "B") + val a = Naming.simple().name("D", "A") + val b = Naming.simple().name("C", "B") assertEquals(-1, a.compareTo(b)) } @Test fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() { - val a = Name.Known.IntelligentFactory.parse("A", null) - val b = Name.Known.IntelligentFactory.parse("B", null) + val a = Naming.intelligent().name("A", null) + val b = Naming.intelligent().name("B", null) assertEquals(-1, a.compareTo(b)) } @Test fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() { - val a = Name.Known.IntelligentFactory.parse("A Brain in a Bottle", null) - val b = Name.Known.IntelligentFactory.parse("Acid Rain", null) - val c = Name.Known.IntelligentFactory.parse("Boralis / Contrastellar", null) - val d = Name.Known.IntelligentFactory.parse("Breathe In", null) + val a = Naming.intelligent().name("A Brain in a Bottle", null) + val b = Naming.intelligent().name("Acid Rain", null) + val c = Naming.intelligent().name("Boralis / Contrastellar", null) + val d = Naming.intelligent().name("Breathe In", null) assertEquals(1, a.compareTo(b)) assertEquals(1, a.compareTo(c)) assertEquals(-1, a.compareTo(d)) @@ -400,9 +379,9 @@ class NameTest { @Test fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() { - val a = Name.Known.IntelligentFactory.parse("15 Step", null) - val b = Name.Known.IntelligentFactory.parse("128 Harps", null) - val c = Name.Known.IntelligentFactory.parse("1969", null) + val a = Naming.intelligent().name("15 Step", null) + val b = Naming.intelligent().name("128 Harps", null) + val c = Naming.intelligent().name("1969", null) assertEquals(-1, a.compareTo(b)) assertEquals(-1, b.compareTo(c)) assertEquals(-2, a.compareTo(c)) @@ -410,28 +389,22 @@ class NameTest { @Test fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() { - val a = Name.Known.SimpleFactory.parse("A", "C") - val b = Name.Known.SimpleFactory.parse("B", null) + val a = Naming.simple().name("A", "C") + val b = Naming.simple().name("B", null) assertEquals(1, a.compareTo(b)) } @Test fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() { - val a = Name.Known.IntelligentFactory.parse("D", "A") - val b = Name.Known.IntelligentFactory.parse("C", "B") + val a = Naming.intelligent().name("D", "A") + val b = Naming.intelligent().name("C", "B") assertEquals(-1, a.compareTo(b)) } - @Test - fun name_unknown() { - val a = Name.Unknown(0) - assertEquals("?", a.thumb) - } - @Test fun name_compareTo_mixed() { - val a = Name.Unknown(0) - val b = Name.Known.IntelligentFactory.parse("A", null) + val a = Name.Unknown(Placeholder.ALBUM) + val b = Naming.intelligent().name("A", null) assertEquals(-1, a.compareTo(b)) } } diff --git a/app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/ReleaseTypeTest.kt similarity index 98% rename from app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/ReleaseTypeTest.kt index 1294e3daf..cd0627547 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/info/ReleaseTypeTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/ReleaseTypeTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.info +package org.oxycblt.musikr.tag import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/interpret/SeparatorsTest.kt similarity index 97% rename from app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/interpret/SeparatorsTest.kt index 440f044e3..3f6ac6dd5 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/SeparatorsTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/interpret/SeparatorsTest.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.musikr.tag.interpret import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagUtilTest.kt similarity index 76% rename from app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt rename to musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagUtilTest.kt index 7c900d42c..bb6f1be08 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TagUtilTest.kt +++ b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagUtilTest.kt @@ -16,10 +16,15 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.metadata +package org.oxycblt.musikr.tag.parse import org.junit.Assert.assertEquals import org.junit.Test +import org.oxycblt.musikr.tag.format.parseId3GenreNames +import org.oxycblt.musikr.tag.format.parseSlashPositionField +import org.oxycblt.musikr.tag.format.parseXiphPositionField +import org.oxycblt.musikr.util.correctWhitespace +import org.oxycblt.musikr.util.splitEscaped class TagUtilTest { @Test @@ -73,39 +78,39 @@ class TagUtilTest { @Test fun parseId3v2PositionField_correct() { - assertEquals(16, "16/32".parseId3v2PositionField()) - assertEquals(16, "16".parseId3v2PositionField()) + assertEquals(16, "16/32".parseSlashPositionField()) + assertEquals(16, "16".parseSlashPositionField()) } @Test fun parseId3v2PositionField_zeroed() { - assertEquals(null, "0".parseId3v2PositionField()) - assertEquals(0, "0/32".parseId3v2PositionField()) + assertEquals(null, "0".parseSlashPositionField()) + assertEquals(0, "0/32".parseSlashPositionField()) } @Test fun parseId3v2PositionField_wack() { - assertEquals(16, "16/".parseId3v2PositionField()) - assertEquals(null, "a".parseId3v2PositionField()) - assertEquals(null, "a/b".parseId3v2PositionField()) + assertEquals(16, "16/".parseSlashPositionField()) + assertEquals(null, "a".parseSlashPositionField()) + assertEquals(null, "a/b".parseSlashPositionField()) } @Test fun parseVorbisPositionField_correct() { - assertEquals(16, parseVorbisPositionField("16", "32")) - assertEquals(16, parseVorbisPositionField("16", null)) + assertEquals(16, parseXiphPositionField("16", "32")) + assertEquals(16, parseXiphPositionField("16", null)) } @Test fun parseVorbisPositionField_zeroed() { - assertEquals(null, parseVorbisPositionField("0", null)) - assertEquals(0, parseVorbisPositionField("0", "32")) + assertEquals(null, parseXiphPositionField("0", null)) + assertEquals(0, parseXiphPositionField("0", "32")) } @Test fun parseVorbisPositionField_wack() { - assertEquals(null, parseVorbisPositionField("a", null)) - assertEquals(null, parseVorbisPositionField("a", "b")) + assertEquals(null, parseXiphPositionField("a", null)) + assertEquals(null, parseXiphPositionField("a", "b")) } @Test diff --git a/settings.gradle b/settings.gradle index 61caa7aa7..9b399cca7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,4 +19,5 @@ gradle.ext.androidxMediaProjectName = 'media-' apply from: file("media/core_settings.gradle") rootProject.name = "Auxio" -include ':app' \ No newline at end of file +include ':app' +include ':musikr'