mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
Compare commits
10 Commits
e448b08b84
...
2aeb816d8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aeb816d8d | ||
|
|
07ad7124a9 | ||
|
|
fa9b42cf10 | ||
|
|
8d4ea82a7e | ||
|
|
eb972c8633 | ||
|
|
aca3aeb592 | ||
|
|
5b36edd259 | ||
|
|
ea01dec6ea | ||
|
|
79266fd483 | ||
|
|
7b85ab1617 |
168
.github/workflows/spotube-release-binary.yml
vendored
168
.github/workflows/spotube-release-binary.yml
vendored
@ -12,6 +12,19 @@ on:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
platform:
|
||||
description: 'Platform to build (nightly only; stable and beta always build everything)'
|
||||
required: true
|
||||
type: choice
|
||||
default: 'all'
|
||||
options:
|
||||
- all
|
||||
- android
|
||||
- linux-x86_64
|
||||
- linux-aarch64
|
||||
- windows
|
||||
- macos
|
||||
- ios
|
||||
dry_run:
|
||||
description: 'Dry run (build & upload artifacts only, skip creating release)'
|
||||
required: false
|
||||
@ -29,16 +42,94 @@ permissions:
|
||||
|
||||
# Never cancel a running release when another is dispatched, and let stable, beta and nightly run side by side
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.release_type }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.release_type }}-${{ inputs.platform }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
COMPOSE_WEBVIEW_REPO: KRTirtho/ComposeNativeWebview
|
||||
COMPOSE_WEBVIEW_COMMIT: cbd36f88927298a28a2d64c6acf7ec3b2def6f9d
|
||||
# fix/linux-wayland-input
|
||||
COMPOSE_WEBVIEW_COMMIT: b11ac748de1219f9432338c067a2c5dafef3072a
|
||||
|
||||
jobs:
|
||||
validate-inputs:
|
||||
name: Validate Inputs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Single platform builds are nightly only
|
||||
if: ${{ inputs.platform != 'all' && inputs.release_type != 'nightly' }}
|
||||
run: |
|
||||
echo "::error::Building a single platform is only supported for nightly releases"
|
||||
exit 1
|
||||
|
||||
# ComposeNativeWebview's JNI backends are gitignored and only built for the host OS, so a
|
||||
# publish from macOS alone ships a jar without the Linux / Windows WebView libraries (the
|
||||
# WebView then renders nothing). Build them on their own OS and hand them to prepare-deps.
|
||||
build-webview-natives:
|
||||
name: WebView Natives (${{ matrix.name }})
|
||||
needs: validate-inputs
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- name: linux-x64
|
||||
os: ubuntu-latest
|
||||
- name: linux-aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
- name: windows-x64
|
||||
os: windows-latest
|
||||
steps:
|
||||
- name: Checkout ComposeNativeWebview
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ env.COMPOSE_WEBVIEW_REPO }}
|
||||
ref: ${{ env.COMPOSE_WEBVIEW_COMMIT }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: compose-webview
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Build Linux WebView native library
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: compose-webview
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential pkg-config libgtk-3-dev libcairo2-dev \
|
||||
libsoup-3.0-dev libwebkit2gtk-4.1-dev
|
||||
bash webview-compose/src/jvmMain/native/linux/build.sh
|
||||
|
||||
- name: Build Windows WebView native library
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: compose-webview
|
||||
shell: cmd
|
||||
run: webview-compose\src\jvmMain\native\windows\build.bat
|
||||
|
||||
- name: Verify WebView native library
|
||||
working-directory: compose-webview
|
||||
shell: bash
|
||||
run: |
|
||||
case "${{ matrix.name }}" in
|
||||
linux-*) files="${{ matrix.name }}/libcompose_webview_linux.so" ;;
|
||||
windows-x64) files="win32-x64/compose_webview_windows.dll win32-x64/WebView2Loader.dll" ;;
|
||||
esac
|
||||
for f in $files; do
|
||||
test -f "webview-compose/src/jvmMain/resources/nucleus/native/$f" || { echo "::error::Missing $f"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Upload WebView native library
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: webview-natives-${{ matrix.name }}
|
||||
path: compose-webview/webview-compose/src/jvmMain/resources/nucleus/native/
|
||||
retention-days: 1
|
||||
|
||||
prepare-deps:
|
||||
name: Prepare Dependencies
|
||||
needs: [validate-inputs, build-webview-natives]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout ComposeNativeWebview
|
||||
@ -49,6 +140,15 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: compose-webview
|
||||
|
||||
# Downloaded after checkout, so they are newer than the native sources and
|
||||
# the build keeps them instead of trying to rebuild on the wrong OS.
|
||||
- name: Download WebView native libraries
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: webview-natives-*
|
||||
merge-multiple: true
|
||||
path: compose-webview/webview-compose/src/jvmMain/resources/nucleus/native
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
@ -70,6 +170,16 @@ jobs:
|
||||
chmod +x gradlew
|
||||
./gradlew publishToMavenLocal
|
||||
|
||||
- name: Verify published JVM jar carries every WebView backend
|
||||
run: |
|
||||
jar=$(ls ~/.m2/repository/dev/nucleusframework/composewebview-jvm/*/composewebview-jvm-*.jar | grep -v -e sources -e javadoc | head -1)
|
||||
contents=$(unzip -l "$jar")
|
||||
for f in linux-x64/libcompose_webview_linux.so linux-aarch64/libcompose_webview_linux.so \
|
||||
win32-x64/compose_webview_windows.dll win32-x64/WebView2Loader.dll \
|
||||
darwin-aarch64/libcompose_webview_macos.dylib darwin-x64/libcompose_webview_macos.dylib; do
|
||||
grep -q "nucleus/native/$f" <<< "$contents" || { echo "::error::$jar is missing $f"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Upload maven local
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@ -78,7 +188,8 @@ jobs:
|
||||
|
||||
build-android:
|
||||
name: Android
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'android' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -152,7 +263,8 @@ jobs:
|
||||
|
||||
build-linux:
|
||||
name: Linux Desktop
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'linux-x86_64' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -161,17 +273,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Download maven local
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: maven-local
|
||||
path: ~/.m2/repository/
|
||||
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
@ -265,7 +377,8 @@ jobs:
|
||||
|
||||
build-linux-arm64:
|
||||
name: Linux Desktop (aarch64)
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'linux-aarch64' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
@ -281,17 +394,17 @@ jobs:
|
||||
with:
|
||||
packages: tools platform-tools ndk;28.2.13676358
|
||||
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Download maven local
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: maven-local
|
||||
path: ~/.m2/repository/
|
||||
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
@ -385,7 +498,8 @@ jobs:
|
||||
|
||||
build-windows:
|
||||
name: Windows Desktop
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'windows' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
@ -448,7 +562,8 @@ jobs:
|
||||
|
||||
build-macos:
|
||||
name: macOS Desktop
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'macos' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
@ -516,7 +631,8 @@ jobs:
|
||||
|
||||
build-ios:
|
||||
name: iOS
|
||||
needs: prepare-deps
|
||||
needs: [validate-inputs, prepare-deps]
|
||||
if: ${{ inputs.platform == 'all' || inputs.platform == 'ios' }}
|
||||
timeout-minutes: 120
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
@ -585,8 +701,11 @@ jobs:
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
# Skipped platform jobs count as skipped, not failed, so a single platform build still releases
|
||||
if: ${{ !inputs.dry_run && !cancelled() && !contains(needs.*.result, 'failure') }}
|
||||
needs:
|
||||
- validate-inputs
|
||||
- prepare-deps
|
||||
- build-android
|
||||
- build-linux
|
||||
- build-linux-arm64
|
||||
@ -599,7 +718,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# The build resolves its plugins from mavenLocal, so Gradle needs the same setup as the build jobs
|
||||
- name: Download maven local
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
@ -639,6 +757,7 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
- name: Generate release notes
|
||||
if: ${{ inputs.platform == 'all' }}
|
||||
run: |
|
||||
echo "## Downloads" > release-notes.md
|
||||
echo "" >> release-notes.md
|
||||
@ -658,9 +777,12 @@ jobs:
|
||||
tag_name: ${{ steps.vars.outputs.tag }}
|
||||
# A new tag (stable) would otherwise point at the default branch instead of the built commit
|
||||
target_commitish: ${{ github.sha }}
|
||||
fail_on_unmatched_files: true
|
||||
name: ${{ steps.vars.outputs.release_name }}
|
||||
body_path: release-notes.md
|
||||
# A single platform build only has its own artifacts
|
||||
fail_on_unmatched_files: ${{ inputs.platform == 'all' }}
|
||||
# A single platform build only replaces that platform's assets (same file names are overwritten),
|
||||
# so leave the existing release's name and notes alone
|
||||
name: ${{ inputs.platform == 'all' && steps.vars.outputs.release_name || '' }}
|
||||
body_path: ${{ inputs.platform == 'all' && 'release-notes.md' || '' }}
|
||||
draft: ${{ steps.vars.outputs.is_draft }}
|
||||
prerelease: ${{ steps.vars.outputs.is_prerelease }}
|
||||
files: |
|
||||
|
||||
@ -366,6 +366,7 @@ kotlin {
|
||||
implementation(libs.nucleus.decorated.window.tao)
|
||||
implementation(libs.compose.native.tray)
|
||||
implementation(libs.nucleus.media.control)
|
||||
implementation(libs.nucleus.darkmode.detector)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -645,6 +646,12 @@ tasks.withType<ComposeHotRun>().configureEach {
|
||||
jvmArgs(
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
)
|
||||
// Linux: the hot-reload agent starts AWT before main(), and JBR's X11 toolkit pins GTK to
|
||||
// XWayland, where the embedded WebKit WebView renders black. Tao owns the windows, so the
|
||||
// reloadable JVM stays headless (DevTools runs in its own JVM and is unaffected).
|
||||
if (System.getProperty("os.name").lowercase().contains("linux")) {
|
||||
jvmArgs("-Djava.awt.headless=true")
|
||||
}
|
||||
systemProperty(
|
||||
"compose.application.resources.dir",
|
||||
project.layout.buildDirectory.dir("compose/tmp/prepareAppResources").get()
|
||||
|
||||
@ -39,4 +39,6 @@ object DebugConfig {
|
||||
* Keep it `false` when committing.
|
||||
*/
|
||||
const val FORCE_SUPPORT_REMINDER: Boolean = false
|
||||
|
||||
const val FORCE_VLC_LOGS: Boolean = false
|
||||
}
|
||||
|
||||
@ -425,7 +425,7 @@ internal class StreamProxy(
|
||||
if (requestMethod == HttpMethod.Head) {
|
||||
call.respond(object : OutgoingContent.NoContent() {
|
||||
override val status: HttpStatusCode = upstream.status
|
||||
override val headers: Headers = downstreamHeaders
|
||||
override val headers: Headers = downstreamHeaders.passthroughForPlayer()
|
||||
override val contentLength: Long? = downstreamContentLength
|
||||
})
|
||||
return
|
||||
@ -446,7 +446,7 @@ internal class StreamProxy(
|
||||
|
||||
call.respond(object : OutgoingContent.WriteChannelContent() {
|
||||
override val status: HttpStatusCode = upstream.status
|
||||
override val headers: Headers = downstreamHeaders
|
||||
override val headers: Headers = downstreamHeaders.passthroughForPlayer()
|
||||
override val contentLength: Long? = downstreamContentLength
|
||||
override val contentType: ContentType? =
|
||||
downstreamHeaders[HttpHeaders.ContentType]?.let(ContentType.Companion::parse)
|
||||
@ -594,4 +594,28 @@ internal class StreamProxy(
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val PLAYER_PASSTHROUGH_HEADERS = listOf(
|
||||
HttpHeaders.ContentRange,
|
||||
HttpHeaders.AcceptRanges,
|
||||
HttpHeaders.ETag,
|
||||
HttpHeaders.LastModified,
|
||||
)
|
||||
|
||||
/**
|
||||
* The upstream headers a player actually needs. Copying them all makes Ktor emit duplicate
|
||||
* Content-Length/Content-Type (also set through the OutgoingContent) and forwards hop-by-hop
|
||||
* headers like `Connection: keep-alive`, which VLC rejects by closing the socket right after
|
||||
* the response headers. Content-Range and Accept-Ranges must stay: without them the player
|
||||
* treats a 206 as invalid and the stream as unseekable, and e.g. WebM can't be demuxed.
|
||||
*/
|
||||
internal fun Headers.passthroughForPlayer(): Headers {
|
||||
val upstream = this
|
||||
return HeadersBuilder().apply {
|
||||
PLAYER_PASSTHROUGH_HEADERS.forEach { name ->
|
||||
upstream.getAll(name)?.forEach { value -> append(name, value) }
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@ -80,7 +80,8 @@ class StreamingUrlRepository(
|
||||
private val audioPlayerQueue: AudioPlayerQueue,
|
||||
private val matchedTracksRepository: TrackSourceRepository,
|
||||
private val settingsRepository: UserSettingsSource,
|
||||
private val streamUrlCacheTtlMs: Long = 30.seconds.inWholeMilliseconds) : KoinComponent {
|
||||
private val streamUrlCacheTtlMs: Long = 30.seconds.inWholeMilliseconds,
|
||||
private val platform: PlatformType = currentPlatform) : KoinComponent {
|
||||
|
||||
private val logger by injectLogger<StreamingUrlRepository>()
|
||||
private val streamUrlCacheMutex = Mutex()
|
||||
@ -145,7 +146,7 @@ class StreamingUrlRepository(
|
||||
} else {
|
||||
stream.streams
|
||||
}
|
||||
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality)
|
||||
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality, platform)
|
||||
.also { logSelection(trackId, origin, candidates, preferredFormat, preferredQuality, it) }
|
||||
}
|
||||
|
||||
@ -259,7 +260,7 @@ class StreamingUrlRepository(
|
||||
"lowest wins):\n" +
|
||||
streams.joinToString("\n") { stream ->
|
||||
val marker = if (stream === selected) " -> " else " "
|
||||
"$marker${stream.describeForLog()} key=(${if (stream.isPlayableOn(currentPlatform)) 0 else 1}, " +
|
||||
"$marker${stream.describeForLog()} key=(${if (stream.isPlayableOn(platform)) 0 else 1}, " +
|
||||
"${formatMismatchCount(stream, preferredFormat)}, " +
|
||||
"${if (stream.matchesQualityType(preferredQuality)) 0 else 1}, " +
|
||||
"${stream.qualityDistance(preferredQuality).let { if (it == Long.MAX_VALUE) "inf" else "$it" }}, " +
|
||||
@ -268,8 +269,8 @@ class StreamingUrlRepository(
|
||||
}
|
||||
when {
|
||||
selected == null -> logger.w { "[Resolve $trackId] no stream selected" }
|
||||
!selected.isPlayableOn(currentPlatform) -> logger.w {
|
||||
"[Resolve $trackId] NO PLAYABLE STREAM on $currentPlatform; falling back to " +
|
||||
!selected.isPlayableOn(platform) -> logger.w {
|
||||
"[Resolve $trackId] NO PLAYABLE STREAM on $platform; falling back to " +
|
||||
"${selected.describeForLog()}, which the player will likely reject"
|
||||
}
|
||||
formatMismatchCount(selected, preferredFormat) > 0 -> logger.w {
|
||||
@ -424,12 +425,16 @@ internal fun selectPreferredAudioStream(
|
||||
}
|
||||
)
|
||||
|
||||
// AVPlayer has no WebM/Ogg/Matroska demuxer, so e.g. YouTube's opus/webm streams fail to load.
|
||||
private val IOS_UNSUPPORTED_CONTAINERS = setOf("webm", "ogg", "oga", "mkv", "matroska")
|
||||
private val MATROSKA_CONTAINERS = setOf("webm", "mkv", "matroska")
|
||||
|
||||
// Containers a platform's player can't open, so e.g. YouTube's opus/webm streams fail to load there.
|
||||
private val UNSUPPORTED_CONTAINERS: Map<PlatformType, Set<String>> = mapOf(
|
||||
// AVPlayer has no WebM/Ogg/Matroska demuxer.
|
||||
PlatformType.IOS to MATROSKA_CONTAINERS + setOf("ogg", "oga"))
|
||||
|
||||
/**
|
||||
* Whether [platform]'s player can play this stream. VLC (desktop) and ExoPlayer (Android)
|
||||
* handle everything plugins return; AVPlayer (iOS) can't demux WebM/Ogg and can't play DASH.
|
||||
* Whether [platform]'s player can play this stream. ExoPlayer (Android) and VLC elsewhere handle
|
||||
* everything plugins return; AVPlayer (iOS) can't demux WebM/Ogg and can't play DASH.
|
||||
*/
|
||||
internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
||||
isContainerPlayableOn(container, platform) &&
|
||||
@ -437,7 +442,7 @@ internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
||||
|
||||
/** Whether [platform]'s player can open [container] at all; see [AudioStream.isPlayableOn]. */
|
||||
internal fun isContainerPlayableOn(container: String, platform: PlatformType): Boolean =
|
||||
platform != PlatformType.IOS || container.lowercase() !in IOS_UNSUPPORTED_CONTAINERS
|
||||
container.lowercase() !in UNSUPPORTED_CONTAINERS[platform].orEmpty()
|
||||
|
||||
private fun formatMismatchCount(stream: AudioStream, preferredFormat: AudioFormat): Int =
|
||||
(if (stream.codec.equals(preferredFormat.codec, ignoreCase = true)) 0 else 1) +
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
package org.spotube.spotube.core.ui.theming
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -205,7 +204,7 @@ fun SpotubeTheme(
|
||||
val isDarkTheme = when (settings.theme) {
|
||||
Theme.LIGHT -> false
|
||||
Theme.DARK -> true
|
||||
Theme.SYSTEM -> isSystemInDarkTheme()
|
||||
Theme.SYSTEM -> isSystemDarkTheme()
|
||||
}
|
||||
val palette: MillenniumPalette = settings.palette
|
||||
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.ui.theming
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/** Whether the OS is in dark mode, observed live; desktop can't rely on Compose's own check under Tao. */
|
||||
@Composable
|
||||
expect fun isSystemDarkTheme(): Boolean
|
||||
@ -108,14 +108,14 @@ data class JamBroker(
|
||||
val connectionTimeoutSeconds: Int = 10)
|
||||
|
||||
/**
|
||||
* opus/webm everywhere except iOS, whose AVPlayer can't open WebM/Ogg, so it streams AAC.
|
||||
* opus/webm wherever the platform's player can open WebM; iOS (AVPlayer) can't, so it streams AAC.
|
||||
* TODO: revisit once iOS can play Opus (e.g. remuxing to CAF or a non-AVPlayer backend).
|
||||
*/
|
||||
internal fun defaultStreamingMusicFormat(platform: PlatformType = currentPlatform): AudioFormat {
|
||||
val ios = platform == PlatformType.IOS
|
||||
val aac = !isContainerPlayableOn("webm", platform)
|
||||
return AudioFormat(
|
||||
codec = if (ios) "aac" else "opus",
|
||||
container = if (ios) "mp4" else "webm",
|
||||
codec = if (aac) "aac" else "opus",
|
||||
container = if (aac) "mp4" else "webm",
|
||||
qualities = listOf(
|
||||
AudioQuality.Lossy(bitrate = 44_000),
|
||||
AudioQuality.Lossy(bitrate = 96_000),
|
||||
|
||||
@ -44,11 +44,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.spotube.spotube.core.ui.theming.AppTheme
|
||||
import org.spotube.spotube.core.ui.theming.isLightAppearance
|
||||
import org.spotube.spotube.modules.settings.SettingsViewModel
|
||||
import org.spotube.spotube.modules.settings.Theme
|
||||
import org.spotube.spotube.modules.settings.UserSettings
|
||||
@ -206,7 +206,8 @@ private fun PaletteSwatch(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit) {
|
||||
val scheme = remember(palette) { MillenniumColorSchemes().resolve(palette, dark = false) }
|
||||
val dark = !isLightAppearance()
|
||||
val scheme = remember(palette, dark) { MillenniumColorSchemes().resolve(palette, dark = dark) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
@ -222,7 +223,8 @@ private fun PaletteSwatch(
|
||||
|
||||
@Composable
|
||||
private fun PaletteTrailingSwatch(palette: MillenniumPalette) {
|
||||
val scheme = remember(palette) { MillenniumColorSchemes().resolve(palette, dark = false) }
|
||||
val dark = !isLightAppearance()
|
||||
val scheme = remember(palette, dark) { MillenniumColorSchemes().resolve(palette, dark = dark) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
@ -241,7 +243,7 @@ private fun PaletteTrailingSwatch(palette: MillenniumPalette) {
|
||||
|
||||
@Composable
|
||||
private fun PalettePreview(palette: MillenniumPalette) {
|
||||
val dark = AppTheme.colors.background.luminance() < 0.5f
|
||||
val dark = !isLightAppearance()
|
||||
MillenniumTheme(dark = dark, config = MillenniumThemeConfig(palette = palette)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.server
|
||||
|
||||
import io.ktor.http.HeadersBuilder
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class StreamProxyHeadersTest {
|
||||
|
||||
// Lowercase names and values as googlevideo sends them
|
||||
private val upstream = HeadersBuilder().apply {
|
||||
append("accept-ranges", "bytes")
|
||||
append("alt-svc", "h3=\":443\"")
|
||||
append("cache-control", "private, max-age=21298")
|
||||
append("connection", "keep-alive")
|
||||
append("content-length", "3523553")
|
||||
append("content-range", "bytes 0-3523552/3523553")
|
||||
append("content-type", "audio/webm")
|
||||
append("last-modified", "Tue, 18 Feb 2025 22:42:11 GMT")
|
||||
}.build()
|
||||
|
||||
@Test
|
||||
fun `keeps the headers a player needs to seek`() {
|
||||
val forwarded = upstream.passthroughForPlayer()
|
||||
assertEquals("bytes 0-3523552/3523553", forwarded["Content-Range"])
|
||||
assertEquals("bytes", forwarded["Accept-Ranges"])
|
||||
assertEquals("Tue, 18 Feb 2025 22:42:11 GMT", forwarded["Last-Modified"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `drops headers that are duplicated or hop-by-hop`() {
|
||||
val forwarded = upstream.passthroughForPlayer()
|
||||
// Length and type are set on the OutgoingContent; forwarding them too emits them twice
|
||||
assertNull(forwarded["Content-Length"])
|
||||
assertNull(forwarded["Content-Type"])
|
||||
assertNull(forwarded["Connection"])
|
||||
assertNull(forwarded["Alt-Svc"])
|
||||
assertNull(forwarded["Cache-Control"])
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import org.spotube.plugin_interfaces.plugin_apis.audio.AudioSource
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.PlatformType
|
||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||
import org.spotube.spotube.modules.settings.UserSettings
|
||||
import co.touchlab.kermit.Logger
|
||||
@ -99,7 +100,9 @@ class StreamingUrlRepositoryBehaviorTest {
|
||||
audioPlayerQueue = queue,
|
||||
matchedTracksRepository = matchedTracksRepository,
|
||||
settingsRepository = settings,
|
||||
streamUrlCacheTtlMs = cacheTtlMs)
|
||||
streamUrlCacheTtlMs = cacheTtlMs,
|
||||
// Not the host's platform: Windows and iOS don't prefer opus/webm
|
||||
platform = PlatformType.Linux)
|
||||
|
||||
fun selectPlugin(pluginId: String, audioAPI: FakeAudioAPI = FakeAudioAPI()): FakeAudioPluginService {
|
||||
val service = FakeAudioPluginService(pluginId, audioAPI)
|
||||
@ -236,6 +239,11 @@ class StreamingUrlRepositoryBehaviorTest {
|
||||
)
|
||||
}
|
||||
|
||||
// The default format depends on the host platform (aac/mp4 on Windows and iOS)
|
||||
h.settings.update {
|
||||
copy(streamingMusicFormat = streamingMusicFormat.copy(codec = "opus", container = "webm"))
|
||||
}
|
||||
|
||||
val first = h.repository.resolveStreamInfo(t)
|
||||
assertEquals("https://example.com/opus.webm", first?.url)
|
||||
|
||||
|
||||
@ -35,8 +35,10 @@ class StreamingFormatDefaultsTest {
|
||||
|
||||
@Test
|
||||
fun `other platforms keep opus in webm`() {
|
||||
val format = defaultStreamingMusicFormat(PlatformType.Android)
|
||||
assertEquals("opus" to "webm", format.codec to format.container)
|
||||
listOf(PlatformType.Android, PlatformType.Linux, PlatformType.MacOS, PlatformType.Windows).forEach { platform ->
|
||||
val format = defaultStreamingMusicFormat(platform)
|
||||
assertEquals("opus" to "webm", format.codec to format.container, platform.name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.spotube.spotube.core.DebugConfig
|
||||
import uk.co.caprica.vlcj.factory.MediaPlayerFactory
|
||||
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
|
||||
import uk.co.caprica.vlcj.medialist.MediaList
|
||||
@ -87,11 +88,13 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
private val _error = MutableSharedFlow<Throwable>(extraBufferCapacity = 1)
|
||||
|
||||
actual override val playerStateFlow: StateFlow<PlayerState> = _playerState.asStateFlow()
|
||||
actual override val currentMediaItemFlow: StateFlow<MediaItem?> = _currentMediaItem.asStateFlow()
|
||||
actual override val currentMediaItemFlow: StateFlow<MediaItem?> =
|
||||
_currentMediaItem.asStateFlow()
|
||||
actual override val playlistFlow: StateFlow<List<MediaItem>> = _playlist.asStateFlow()
|
||||
actual override val durationFlow: StateFlow<Duration> = _duration.asStateFlow()
|
||||
actual override val positionFlow: StateFlow<Duration> = _position.asStateFlow()
|
||||
actual override val bufferingPositionFlow: StateFlow<Duration> = _bufferingPosition.asStateFlow()
|
||||
actual override val bufferingPositionFlow: StateFlow<Duration> =
|
||||
_bufferingPosition.asStateFlow()
|
||||
actual override val loopStateFlow: StateFlow<LoopState> = _loopState.asStateFlow()
|
||||
actual override val shuffleModeFlow: StateFlow<Boolean> = _shuffleMode.asStateFlow()
|
||||
actual override val playbackSpeedFlow: StateFlow<Float> = _playbackSpeed.asStateFlow()
|
||||
@ -101,6 +104,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
|
||||
init {
|
||||
VLCBundleLoaderGenerated.getVerifiedPath()
|
||||
val vlcLogArgs = if (DebugConfig.FORCE_VLC_LOGS) arrayOf("--verbose=2") else emptyArray()
|
||||
mediaPlayerFactory = MediaPlayerFactory(
|
||||
null as NativeDiscovery?,
|
||||
"--no-video",
|
||||
@ -108,8 +112,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
"--aout=any",
|
||||
"--no-osd",
|
||||
"--no-snapshot-preview",
|
||||
// "--verbose=2"
|
||||
// "--quiet"
|
||||
*vlcLogArgs
|
||||
)
|
||||
audioListPlayerComponent = object : AudioListPlayerComponent(mediaPlayerFactory) {
|
||||
override fun opening(mediaPlayer: MediaPlayer) {
|
||||
@ -122,7 +125,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
) {
|
||||
val durationMs = mediaPlayer.status().length()
|
||||
_bufferingPosition.tryEmit(
|
||||
if (durationMs > 0) (durationMs * (newCache / 100f)).toLong().milliseconds else Duration.ZERO)
|
||||
if (durationMs > 0) (durationMs * (newCache / 100f)).toLong().milliseconds else Duration.ZERO
|
||||
)
|
||||
_playerState.tryEmit(if (newCache < 100f) PlayerState.BUFFERING else PlayerState.READY)
|
||||
}
|
||||
|
||||
@ -184,7 +188,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
|
||||
override fun nextItem(
|
||||
mediaListPlayer: uk.co.caprica.vlcj.player.list.MediaListPlayer,
|
||||
item: uk.co.caprica.vlcj.media.MediaRef) {
|
||||
item: uk.co.caprica.vlcj.media.MediaRef
|
||||
) {
|
||||
lock.withLock {
|
||||
val targetIndex = pendingNextIndex ?: run {
|
||||
val currentMrl = runCatching {
|
||||
@ -336,7 +341,11 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
}
|
||||
}
|
||||
|
||||
actual override suspend fun load(playlist: List<MediaItem>, autoPlay: Boolean, startPosition: Int) {
|
||||
actual override suspend fun load(
|
||||
playlist: List<MediaItem>,
|
||||
autoPlay: Boolean,
|
||||
startPosition: Int
|
||||
) {
|
||||
lock.withLock {
|
||||
if (disposed) return
|
||||
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.ui.theming
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode
|
||||
|
||||
// The Tao window never feeds Compose's LocalSystemTheme, so ask the OS directly
|
||||
@Composable
|
||||
actual fun isSystemDarkTheme(): Boolean = isSystemInDarkMode()
|
||||
@ -132,6 +132,15 @@ private fun restoreWindowState(saved: PersistedWindowState?): PersistedWindowSta
|
||||
return PersistedWindowState.of(size, saved.placement)
|
||||
}
|
||||
|
||||
/** Runs [block] on a fresh thread and waits for it, rethrowing whatever it threw. */
|
||||
private fun runOnWorkerThread(name: String, block: () -> Unit) {
|
||||
var failure: Throwable? = null
|
||||
val thread = Thread({ runCatching(block).onFailure { failure = it } }, name)
|
||||
thread.start()
|
||||
thread.join()
|
||||
failure?.let { throw it }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, FlowPreview::class)
|
||||
fun main() {
|
||||
// Sets compose.application.resources.dir in the native image. initKoin() creates the audio
|
||||
@ -140,8 +149,19 @@ fun main() {
|
||||
GraalVmInitializer.initialize()
|
||||
AppImageEnvironment.apply()
|
||||
FileKit.init(appId = "org.spotube.spotube")
|
||||
val initApp = {
|
||||
initKoin()
|
||||
NewPipeDownloader.init(KoinPathsProvider.paths)
|
||||
}
|
||||
// libvlc and the system media controls initialize COM as multithreaded on the calling thread. In the
|
||||
// native image the main thread also creates the Tao window, whose OleInitialize needs a single-threaded
|
||||
// apartment and panics with RPC_E_CHANGED_MODE, so on Windows Koin's eager singletons are created
|
||||
// elsewhere. COM doesn't exist on macOS/Linux, where this stays on the main thread as before.
|
||||
if (getPlatform().type == PlatformType.Windows) {
|
||||
runOnWorkerThread("spotube-init", initApp)
|
||||
} else {
|
||||
initApp()
|
||||
}
|
||||
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
nucleusApplication(backend = NucleusBackend.Tao) {
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.ui.theming
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun isSystemDarkTheme(): Boolean = isSystemInDarkTheme()
|
||||
@ -14,16 +14,12 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
[versions]
|
||||
agp = "9.1.0"
|
||||
android-compileSdk = "37"
|
||||
android-minSdk = "28"
|
||||
android-targetSdk = "36"
|
||||
android-targetSdk = "37"
|
||||
agp = "9.1.0"
|
||||
androidx-activity = "1.13.0"
|
||||
androidx-appcompat = "1.7.1"
|
||||
androidx-core = "1.19.0"
|
||||
androidx-espresso = "3.7.0"
|
||||
androidx-lifecycle = "2.10.0"
|
||||
androidx-testExt = "1.3.0"
|
||||
appdirs = "1.5.0"
|
||||
cache4k = "0.14.0"
|
||||
carApp = "1.7.0"
|
||||
@ -34,12 +30,10 @@ composeShimmer = "1.5.0"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
haze = "2.0.0-alpha03"
|
||||
jna = "5.18.1"
|
||||
junit = "4.13.2"
|
||||
kermit = "2.1.0"
|
||||
kmpZip = "1.0.0"
|
||||
kotlin = "2.4.21"
|
||||
kotlinx-coroutines = "1.11.0"
|
||||
kotlinx-io = "0.9.1"
|
||||
kotlinx-serialization-json = "1.11.0"
|
||||
millennium = "0.1.1"
|
||||
mqttClient = "2.1.1"
|
||||
@ -55,27 +49,22 @@ vlcjNative = "4.12.0"
|
||||
ziplineVersion = "1.28.0"
|
||||
ktor = "3.5.1"
|
||||
dnssdkt = "1.1.0"
|
||||
runner = "1.7.0"
|
||||
core = "1.7.0"
|
||||
filekit = "0.14.2"
|
||||
compose-webview = "0.1.0-SNAPSHOT"
|
||||
composeNativeTray = "2.1.6"
|
||||
koin = "4.2.2"
|
||||
multiplatform-nav3-ui = "1.1.1"
|
||||
compose-multiplatform-adaptive = "1.3.0-alpha05"
|
||||
compose-multiplatform-lifecycle = "2.10.0"
|
||||
feather-icons = "1.1.1"
|
||||
kotlinx-datetime = "0.8.0"
|
||||
media3 = "1.10.1"
|
||||
androidx-datastore = "1.2.1"
|
||||
ksp = "2.3.12"
|
||||
spotube-gradle = "0.1.2"
|
||||
cryptography = "0.6.0"
|
||||
reorderable = "3.1.0"
|
||||
vlcjBundler = "0.2.0"
|
||||
vlcjBundler = "0.2.1"
|
||||
vanniktechMavenPublish = "0.37.0"
|
||||
uniffi = "0.3.7"
|
||||
kotlinx-atomicfu = "0.33.0"
|
||||
mokkery = "3.5.1"
|
||||
jmustache = "1.16"
|
||||
|
||||
@ -98,12 +87,6 @@ kmp-zip = { module = "no.synth:kmp-zip", version.ref = "kmpZip" }
|
||||
kmp-zip-kotlinx = { module = "no.synth:kmp-zip-kotlinx", version.ref = "kmpZip" }
|
||||
kmp-zip-okio = { module = "no.synth:kmp-zip-okio", version.ref = "kmpZip" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
@ -134,6 +117,7 @@ newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version
|
||||
nucleus-core-runtime = { module = "dev.nucleusframework:nucleus.core-runtime", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-decorated-window-tao = { module = "dev.nucleusframework:nucleus.decorated-window-tao", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-media-control = { module = "dev.nucleusframework:nucleus.media-control", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-darkmode-detector = { module = "dev.nucleusframework:nucleus.darkmode-detector", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-graalvm-runtime = { module = "dev.nucleusframework:nucleus.graalvm-runtime", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-nucleus-application = { module = "dev.nucleusframework:nucleus.nucleus-application", version.ref = "nucleusNucleusApplication" }
|
||||
semver = { module = "net.swiftzer.semver:semver", version.ref = "semver" }
|
||||
@ -152,8 +136,6 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
|
||||
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
||||
androidx-core = { group = "androidx.test", name = "core", version.ref = "core" }
|
||||
filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
|
||||
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
||||
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
||||
@ -169,7 +151,6 @@ feather-icons = { module = "br.com.devsrsouza.compose.icons:feather", version.re
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidx-datastore" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
kotlinx-io-okio = { module = "org.jetbrains.kotlinx:kotlinx-io-okio", version.ref = "kotlinx-io" }
|
||||
cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography" }
|
||||
cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography" }
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
@ -188,10 +169,8 @@ kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", versio
|
||||
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
|
||||
zipline-gradle-plugin = { id = "app.cash.zipline", version.ref = "ziplineVersion" }
|
||||
mavenPublish = { id = "maven-publish" }
|
||||
spotubeGradle = { id = "org.spotube.gradle-plugin", version.ref = "spotube-gradle" }
|
||||
vlcjBundler = { id = "org.spotube.vlcj-bundler", version.ref = "vlcjBundler" }
|
||||
kotlinxAtomicFu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "kotlinx-atomicfu" }
|
||||
uniffi = { id = "dev.gobley.uniffi", version.ref = "uniffi" }
|
||||
cargo = { id = "dev.gobley.cargo", version.ref = "uniffi" }
|
||||
mokkery = { id = "dev.mokkery", version.ref = "mokkery" }
|
||||
|
||||
405
lib/l10n/app_pa.arb
Normal file
405
lib/l10n/app_pa.arb
Normal file
@ -0,0 +1,405 @@
|
||||
{
|
||||
"guest": "ਮਹਿਮਾਨ",
|
||||
"browse": "ਬ੍ਰਾਊਜ਼ ਕਰੋ",
|
||||
"search": "ਖੋਜੋ",
|
||||
"library": "ਲਾਇਬ੍ਰੇਰੀ",
|
||||
"lyrics": "ਗੀਤਾਂ ਦੇ ਬੋਲ",
|
||||
"settings": "ਸੈਟਿੰਗਾਂ",
|
||||
"genre_categories_filter": "ਸ਼੍ਰੇਣੀਆਂ ਜਾਂ ਸ਼ੈਲੀਆਂ ਨੂੰ ਛਾਣੋ...",
|
||||
"genre": "ਸ਼ੈਲੀ",
|
||||
"personalized": "ਵਿਅਕਤੀਗਤ",
|
||||
"featured": "ਵਿਸ਼ੇਸ਼ ਰੂਪ ਨਾਲ ਪ੍ਰਦਰਸ਼ਿਤ",
|
||||
"new_releases": "ਨਵੀਂ ਰਿਲੀਜ਼",
|
||||
"songs": "ਗੀਤ",
|
||||
"playing_track": "{track} ਚੱਲ ਰਿਹਾ ਹੈ",
|
||||
"queue_clear_alert": "ਇਹ ਮੌਜੂਦਾ ਕਤਾਰ ਨੂੰ ਸਾਫ਼ ਕਰ ਦੇਵੇਗਾ। {track_length} ਟ੍ਰੈਕ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"load_more": "ਹੋਰ ਲੋਡ ਕਰੋ",
|
||||
"playlists": "ਪਲੇਸੂਚੀ",
|
||||
"artists": "ਕਲਾਕਾਰ",
|
||||
"albums": "ਐਲਬਮ",
|
||||
"tracks": "ਟ੍ਰੈਕ",
|
||||
"downloads": "ਡਾਊਨਲੋਡ",
|
||||
"filter_playlists": "ਆਪਣੀਆਂ ਪਲੇਸੂਚੀਆਂ ਨੂੰ ਛਾਣੋ...",
|
||||
"liked_tracks": "ਪਸੰਦੀਦਾ ਟ੍ਰੈਕ",
|
||||
"liked_tracks_description": "ਤੁਹਾਡੇ ਸਾਰੇ ਪਸੰਦੀਦਾ ਟ੍ਰੈਕ",
|
||||
"create_playlist": "ਪਲੇਸੂਚੀ ਬਣਾਉ",
|
||||
"create_a_playlist": "ਇੱਕ ਪਲੇਸੂਚੀ ਬਣਾਉ",
|
||||
"create": "ਬਣਾਉ",
|
||||
"cancel": "ਰੱਦ ਕਰੋ",
|
||||
"playlist_name": "ਪਲੇਸੂਚੀ ਦਾ ਨਾਮ",
|
||||
"name_of_playlist": "ਪਲੇਸੂਚੀ ਦਾ ਨਾਮ",
|
||||
"description": "ਵੇਰਵਾ",
|
||||
"public": "ਜਨਤਕ",
|
||||
"collaborative": "ਸਹਿਯੋਗੀ",
|
||||
"search_local_tracks": "ਸਥਾਨਕ ਟ੍ਰੈਕ ਖੋਜੋ...",
|
||||
"play": "ਚਲਾਉ",
|
||||
"delete": "ਹਟਾਉ",
|
||||
"none": "ਕੋਈ ਨਹੀਂ",
|
||||
"sort_a_z": "A-Z ਦਿਖਾਉ",
|
||||
"sort_z_a": "Z-A ਦਿਖਾਉ",
|
||||
"sort_artist": "ਕਲਾਕਾਰ ਅਨੁਸਾਰ ਦਿਖਾਉ",
|
||||
"sort_album": "ਐਲਬਮ ਅਨੁਸਾਰ ਦਿਖਾਉ",
|
||||
"sort_tracks": "ਟ੍ਰੈਕ ਅਨੁਸਾਰ ਦਿਖਾਉ",
|
||||
"currently_downloading": "ਹੁਣ ਡਾਊਨਲੋਡ ਹੋ ਰਿਹਾ ਹੈ ({tracks_length})",
|
||||
"cancel_all": "ਸਾਰਿਆਂ ਨੂੰ ਰੱਦ ਕਰੋ",
|
||||
"filter_artist": "ਕਲਾਕਾਰ ਨੂੰ ਛਾਣੋ...",
|
||||
"followers": "{followers} ਫ਼ਾਲੋਅਰ",
|
||||
"add_artist_to_blacklist": "ਕਲਾਕਾਰ ਨੂੰ ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"top_tracks": "ਚੋਟੀ ਦਾ ਟ੍ਰੈਕ",
|
||||
"fans_also_like": "ਫੈਨਸ ਨੂੰ ਇਹ ਵੀ ਪਸੰਦ ਹੈ",
|
||||
"loading": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
|
||||
"artist": "ਕਲਾਕਾਰ",
|
||||
"blacklisted": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਹੈ",
|
||||
"following": "ਫ਼ਾਲੋ ਕਰ ਰਿਹਾ ਹੈ",
|
||||
"follow": "ਫ਼ਾਲੋ ਕਰੋ",
|
||||
"artist_url_copied": "ਕਲਾਕਾਰ ਯੂਆਰਐਲ ਕਲਿੱਪਬੋਰਡ ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
|
||||
"added_to_queue": "{tracks} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਜੋੜੇ ਗਏ",
|
||||
"filter_albums": "ਐਲਬਮਾਂ ਨੂੰ ਛਾਣੋ...",
|
||||
"synced": "ਸਿੰਕ ਕੀਤਾ ਗਿਆ",
|
||||
"plain": "ਸਾਦਾ",
|
||||
"shuffle": "ਸ਼ਫਲ",
|
||||
"search_tracks": "ਟ੍ਰੈਕ ਖੋਜੋ...",
|
||||
"released": "ਜਾਰੀ ਹੋਇਆ",
|
||||
"error": "ਤਰੁੱਟੀ {error}",
|
||||
"title": "ਸਿਰਲੇਖ",
|
||||
"time": "ਸਮਾਂ",
|
||||
"more_actions": "ਹੋਰ ਕਾਰਵਾਈ",
|
||||
"download_count": "ਡਾਊਨਲੋਡ ({count})",
|
||||
"add_count_to_playlist": "({count}) ਨੂੰ ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"add_count_to_queue": "({count}) ਨੂੰ ਕਤਾਰ ਵਿੱਚ ਜੋੜੋ",
|
||||
"play_count_next": "({count}) ਅਗਲਾ ਚਲਾਉ",
|
||||
"album": "ਐਲਬਮ",
|
||||
"copied_to_clipboard": "{data} ਕਲਿੱਪਬੋਰਡ ਉੱਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
|
||||
"add_to_following_playlists": "{track} ਨੂੰ ਹੇਠ ਲਿਖੀ ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"add": "ਜੋੜੋ",
|
||||
"added_track_to_queue": "{track} ਨੂੰ ਕਤਾਰ ਵਿੱਚ ਜੋੜਿਆ ਗਿਆ",
|
||||
"add_to_queue": "ਕਤਾਰ ਵਿੱਚ ਜੋੜੋ",
|
||||
"track_will_play_next": "{track} ਅਗਲਾ ਚਲਾਉ",
|
||||
"play_next": "ਅਗਲਾ ਚਲਾਉ",
|
||||
"removed_track_from_queue": "{track} ਨੂੰ ਕਤਾਰ ਵਿੱਚੋਂ ਹਟਾਇਆ ਗਿਆ",
|
||||
"remove_from_queue": "ਕਤਾਰ ਵਿੱਚ ਹਟਾਉ",
|
||||
"remove_from_favorites": "ਪਸੰਦੀਦਾ ਵਿੱਚੋਂ ਹਟਾਉ",
|
||||
"save_as_favorite": "ਪਸੰਦੀਦਾ ਵਜੋਂ ਸੰਭਾਲੋ",
|
||||
"add_to_playlist": "ਪਲੇਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"remove_from_playlist": "ਪਲੇਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਉ",
|
||||
"add_to_blacklist": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"remove_from_blacklist": "ਕਾਲੀਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਉ",
|
||||
"share": "ਸਾਂਝਾ ਕਰੋ",
|
||||
"mini_player": "ਮਿੰਨੀ ਪਲੇਅਰ",
|
||||
"slide_to_seek": "ਅੱਗੇ ਜਾਂ ਪਿੱਛੇ ਖੋਜਣ ਲਈ ਸਲਾਈਡ ਕਰੋ",
|
||||
"shuffle_playlist": "ਪਲੇਸੂਚੀ ਸ਼ਫਲ ਕਰੋ",
|
||||
"unshuffle_playlist": "ਪਲੇਸੂਚੀ ਅਣਸ਼ਫਲ ਕਰੋ",
|
||||
"previous_track": "ਪਿਛਲਾ ਟ੍ਰੈਕ",
|
||||
"next_track": "ਅਗਲਾ ਟ੍ਰੈਕ",
|
||||
"pause_playback": "ਪਲੇਬੈਕ ਰੋਕੋ",
|
||||
"resume_playback": "ਪਲੇਬੈਕ ਚਲਾਉ",
|
||||
"loop_track": "ਲੂਪ ਟ੍ਰੈਕ",
|
||||
"repeat_playlist": "ਪਲੇਸੂਚੀ ਦੁਹਰਾਉ",
|
||||
"queue": "ਕਤਾਰ",
|
||||
"alternative_track_sources": "ਵਿਕਲਪਕ ਟ੍ਰੈਕ ਸਰੋਤ",
|
||||
"download_track": "ਟ੍ਰੈਕ ਡਾਊਨਲੋਡ ਕਰੋ",
|
||||
"tracks_in_queue": "{tracks} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਹਨ",
|
||||
"clear_all": "ਸਾਰੇ ਹਟਾਉ",
|
||||
"show_hide_ui_on_hover": "ਹਵਰ ਉੱਤੇ ਯੂਆਈ ਦਿਖਾਉ/ਛਿਪਾਉ",
|
||||
"always_on_top": "ਹਮੇਸ਼ਾ ਉੱਤੇ ਹੋਵੇ",
|
||||
"exit_mini_player": "ਮਿੰਨੀ ਪਲੇਅਰ ਤੋਂ ਬਾਹਰ ਨਿਕਲੋ",
|
||||
"download_location": "ਡਾਊਨਲੋਡ ਸਥਾਨ",
|
||||
"account": "ਖ਼ਾਤਾ",
|
||||
"login_with_spotify": "ਆਪਣੇ ਸਪੋਟਫਾਈ ਖ਼ਾਤੇ ਨਾਲ ਦਾਖ਼ਲ ਹੋਵੋ",
|
||||
"connect_with_spotify": "ਸਪੋਟਫਾਈ ਨਾਲ ਕਨੈਕਟ ਕਰੋ",
|
||||
"logout": "ਵਿਦਾਈ ਲਵੋ",
|
||||
"logout_of_this_account": "ਇਸ ਖ਼ਾਤੇ ਤੋਂ ਵਿਦਾਈ ਲਵੋ",
|
||||
"language_region": "ਭਾਸ਼ਾ ਅਤੇ ਖੇਤਰ",
|
||||
"language": "ਭਾਸ਼ਾ",
|
||||
"system_default": "ਸਿਸਟਮ ਡਿਫਾਲਟ",
|
||||
"market_place_region": "ਮਾਰਕਿਟਪਲੇਸ ਖੇਤਰ",
|
||||
"recommendation_country": "ਸੁਝਾਇਆ ਗਿਆ ਦੇਸ਼",
|
||||
"appearance": "ਦਿੱਖ",
|
||||
"layout_mode": "ਲੇਆਊਟ ਮੋਡ",
|
||||
"override_layout_settings": "ਓਵਰਰਾਈਡ ਰਿਸਪਾਂਸਿਵ ਲੇਆਊਟ ਮੋਡ ਸੈਟਿੰਗ",
|
||||
"adaptive": "ਅਨੁਕੂਲ",
|
||||
"compact": "ਕੰਪੈਕਟ",
|
||||
"extended": "ਵਿਸਤ੍ਰਿਤ",
|
||||
"theme": "ਥੀਮ",
|
||||
"dark": "ਗੂੜ੍ਹਾ",
|
||||
"light": "ਚਾਨਣ",
|
||||
"system": "ਸਿਸਟਮ",
|
||||
"accent_color": "ਅੱਖਰਸ਼ੈਲੀ ਦਾ ਰੰਗ",
|
||||
"sync_album_color": "ਐਲਬਮ ਦਾ ਰੰਗ ਸਿੰਕ ਕਰੋ",
|
||||
"sync_album_color_description": "ਐਲਬਮ ਕਲਾ ਦਾ ਪ੍ਰਾਇਮਰੀ ਰੰਗ ਇੱਕ ਲਹਿਜ਼ੇ ਦੇ ਰੰਗ ਵਜੋਂ ਵਰਤਿਆ ਜਾਂਦਾ ਹੈ",
|
||||
"playback": "ਪਲੇਬੈਕ",
|
||||
"audio_quality": "ਆਡੀਉ ਗੁਣਵੱਤਾ",
|
||||
"high": "ਉੱਚ",
|
||||
"low": "ਨਿਮਨ",
|
||||
"pre_download_play": "ਪਹਿਲਾਂ ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਚਲਾਉ",
|
||||
"pre_download_play_description": "ਸਟ੍ਰੀਮਿੰਗ ਆਡੀਓ ਦੀ ਬਜਾਏ ਬਾਈਟ ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਚਲਾਓ (ਉੱਚ ਬੈਂਡਵਿਡਥ ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਸਿਫ਼ਾਰਿਸ਼ ਕੀਤੀ ਗਈ ਹੈ)",
|
||||
"skip_non_music": "ਗੀਤ ਦੇ ਇਲਾਵਾ ਸੈਗਮੈਂਟਾਂ ਨੂੰ ਛੱਡੋ (ਸਪਾਂਸਰਬਲਾਕ)",
|
||||
"blacklist_description": "ਕਾਲੀਸੂਚੀ ਵਿੱਚ ਸ਼ਾਮਲ ਟ੍ਰੈਕ ਅਤੇ ਕਲਾਕਾਰ",
|
||||
"wait_for_download_to_finish": "ਵਰਤਮਾਨ ਡਾਊਨਲੋਡ ਸਮਾਪਤ ਹੋਣ ਤੱਕ ਇੰਤਜ਼ਾਰ ਕਰੋ",
|
||||
"desktop": "ਡੈਸਕਟਾਪ",
|
||||
"close_behavior": "ਬੰਦ ਕਰਨ ਦਾ ਵਿਵਹਾਰ",
|
||||
"close": "ਬੰਦ ਕਰੋ",
|
||||
"minimize_to_tray": "ਟ੍ਰੇ ਵਿੱਚ ਘੱਟ ਕਰੋ",
|
||||
"show_tray_icon": "ਸਿਸਟਮ ਟ੍ਰੇ ਆਈਕਨ ਦਿਖਾਉ",
|
||||
"about": "ਬਾਬਤ",
|
||||
"u_love_spotube": "ਅਸੀਂ ਜਾਣਦੇ ਹਾਂ ਕਿ ਤੁਸੀਂ Spotube ਨੂੰ ਪਿਆਰ ਕਰਦੇ ਹੋ",
|
||||
"check_for_updates": "ਅੱਪਡੇਟ ਲਈ ਜਾਂਚ ਕਰੋ",
|
||||
"about_spotube": "Spotube ਦੇ ਬਾਰੇ",
|
||||
"blacklist": "ਕਾਲੀਸੂਚੀ",
|
||||
"please_sponsor": "ਕਿਰਪਾ ਕਰਕੇ ਸਪਾਂਸਰ / ਦਾਨ ਕਰੋ",
|
||||
"spotube_description": "Spotube, ਇੱਕ ਹਲਕਾ, ਸਾਰੇ ਪਲੇਟਫਾਰਮਾਂ ਤੇ ਚੱਲਣ ਵਾਲਾ, ਮੁਫ਼ਤ ਸਪੋਟੀਫਾਈ ਕਲਾਇੰਟ",
|
||||
"version": "ਸੰਸਕਰਨ",
|
||||
"build_number": "ਬਿਲਡ ਨੰਬਰ",
|
||||
"founder": "ਸੰਸਥਾਪਕ",
|
||||
"repository": "ਭੰਡਾਰ",
|
||||
"bug_issues": "ਦਿੱਕਤਾਂ+ਮੁੱਦੇ",
|
||||
"made_with": "ਬੰਗਲਾਦੇਸ਼🇧🇩 ਵਿੱਚ ਦਿਲ ਨਾਲ ਬਣਾਇਆ ਗਿਆ",
|
||||
"kingkor_roy_tirtho": "ਕਿੰਗਕੋਰ ਰੌਏ ਤਿਰਥੋ",
|
||||
"copyright": "© 2021-{current_year} ਕਿੰਗਕੋਰ ਰੌਏ ਤਿਰਥੋ",
|
||||
"license": "ਲਸੰਸ",
|
||||
"add_spotify_credentials": "ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਤੁਹਾਡੇ ਸਪੋਟੀਫਾਈ ਕਰੇਡੈਂਸ਼ੀਅਲ ਜੋੜੋ",
|
||||
"credentials_will_not_be_shared_disclaimer": "ਚਿੰਤਾ ਨਾ ਕਰੋ, ਤੁਹਾਡੇ ਕਰੇਡੈਂਸ਼ੀਅਲ ਕਿਸੇ ਵੀ ਤਰ੍ਹਾਂ ਨਾਲ ਇਕੱਠੇ ਜਾਂ ਸਾਂਝੇ ਨਹੀਂ ਕੀਤੇ ਜਾਣਗੇ",
|
||||
"know_how_to_login": "ਇਸਨੂੰ ਕਿਵੇਂ ਕਰਨਾ ਹੈ, ਨਹੀਂ ਪਤਾ?",
|
||||
"follow_step_by_step_guide": "ਕਦਮ ਨਾਲ ਕਦਮ ਗਾਈਡ ਦੇ ਨਾਲ ਚੱਲੋ",
|
||||
"spotify_cookie": "ਸਪੋਟੀਫਾਈ {name} ਕੁਕੀ",
|
||||
"cookie_name_cookie": "{name} ਕੁਕੀ",
|
||||
"fill_in_all_fields": "ਕ੍ਰਿਪਾ ਸਾਰੇ ਫੀਲਡ ਭਰੋ",
|
||||
"submit": "ਸਪੁਰਦ",
|
||||
"exit": "ਬਾਹਰ ਨਿਕਲੋ",
|
||||
"previous": "ਪਿਛਲਾ",
|
||||
"next": "ਅਗਲਾ",
|
||||
"done": "ਹੋ ਗਿਆ",
|
||||
"step_1": "ਚਰਣ 1",
|
||||
"first_go_to": "ਪਹਿਲਾਂ, ਜਾਉ",
|
||||
"login_if_not_logged_in": "ਅਤੇ ਜੇਕਰ ਤੁਸੀਂ ਦਾਖ਼ਲ ਨਹੀਂ ਹੋਏ ਹੋ ਤਾਂ ਦਾਖ਼ਲ ਹੋਵੋ / ਸਾਈਨਅੱਪ ਕਰੋ",
|
||||
"step_2": "ਚਰਣ 2",
|
||||
"step_2_steps": "1. ਇੱਕ ਵਾਰ ਜਦੋਂ ਤੁਸੀਂ ਦਾਖ਼ਲ ਹੋ ਜਾਂਦੇ ਹੋ, ਤਾਂ F12 ਦਬਾਓ ਜਾਂ ਮਾਊਸ ਸੱਜਾ ਕਲਿੱਕ ਕਰੋ > ਜਾਂਚ ਕਰੋ ਤਾਂ ਕਿ ਬ੍ਰਾਊਜ਼ਰ DevTools ਖੁੱਲ੍ਹ ਜਾਵੇ।\n2. ਫਿਰ ਬ੍ਰਾਊਜ਼ਰ ਦੇ \"ਐਪਲੀਕੇਸ਼ਨ\" ਟੈਬ (Chrome, Edge, Brave ਆਦਿ) ਜਾਂ \"ਸਟੋਰੇਜ\" ਟੈਬ (Firefox, Palemoon ਆਦਿ) ਵਿੱਚ ਜਾਉ\n3. \"ਕੁਕੀਜ\" ਅਨੁਭਾਗ ਵਿੱਚ ਜਾਉ ਫਿਰ \"https: //accounts.spotify.com\" ਉਪ-ਅਨੁਭਾਗ ਵਿੱਚ ਜਾਉ",
|
||||
"step_3": "ਚਰਣ 3",
|
||||
"success_emoji": "ਸਫ਼ਲ🥳",
|
||||
"success_message": "ਤੁਸੀਂ ਹੁਣ ਆਪਣੇ Spotify ਖ਼ਾਤੇ ਨਾਲ ਸਫ਼ਲਤਾਪੂਰਵਕ ਦਾਖ਼ਲ ਹੋ ਗਏ ਹੋ। ਤੁਸੀਂ ਵਧੀਆ ਕੰਮ ਕੀਤਾ!",
|
||||
"step_4": "ਚਰਣ 4",
|
||||
"something_went_wrong": "ਕੁੱਝ ਗਲਤ ਹੋ ਗਿਆ ਹੈ",
|
||||
"piped_instance": "ਪਾਈਪਡ ਸਰਵਰ",
|
||||
"piped_description": "ਪਾਈਪ ਕੀਤੇ ਗਏ ਸਰਵਰ",
|
||||
"piped_warning": "ਗਾਣਿਆਂ ਨਾਲ ਮੇਲ ਕਰਨ ਲਈ ਵਰਤੇ ਜਾਂਦੇ ਹਨ, ਹੋ ਸਕਦਾ ਹੈ ਕਿ ਉਹਨਾਂ ਵਿੱਚੋਂ ਕੁਝ ਦੇ ਨਾਲ ਸਹੀ ਢੰਗ ਨਾਲ ਕੰਮ ਨਾ ਕਰੇ ਇਸਲਈ ਆਪਣੇ ਜੋਖਮ 'ਤੇ ਵਰਤੋਂ",
|
||||
"generate_playlist": "ਪਲੇਸੂਚੀ ਬਣਾਉ",
|
||||
"track_exists": "ਟ੍ਰੈਕ {track} ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ",
|
||||
"replace_downloaded_tracks": "ਸਾਰੇ ਡਾਊਨਲੋਡ ਕੀਤੇ ਗਏ ਟ੍ਰੈਕ ਬਦਲੋ",
|
||||
"skip_download_tracks": "ਸਾਰੇ ਡਾਊਨਲੋਡ ਕੀਤੇ ਗਏ ਟ੍ਰੈਕ ਛੱਡੋ",
|
||||
"do_you_want_to_replace": "ਕੀ ਤੁਸੀਂ ਮੌਜੂਦਾ ਟ੍ਰੈਕ ਬਦਲਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"replace": "ਬਦਲੋ",
|
||||
"skip": "ਛੱਡੋ",
|
||||
"select_up_to_count_type": "{count} {type} ਤੱਕ ਚੁਣੋ",
|
||||
"select_genres": "ਸ਼ੈਲੀਆਂ ਚੁਣੋ",
|
||||
"add_genres": "ਸ਼ੈਲੀਆਂ ਜੋੜੋ",
|
||||
"country": "ਦੇਸ਼",
|
||||
"number_of_tracks_generate": "ਉਤਪੰਨ ਕਰਨ ਲਈ ਟ੍ਰੈਕਾਂ ਦੀ ਸੰਖਿਆ",
|
||||
"acousticness": "ਧੁਨਿਕਤਾ",
|
||||
"danceability": "ਨੱਚਣ ਵਾਲੇ",
|
||||
"energy": "ਊਰਜਾ",
|
||||
"instrumentalness": "ਅਲਾਪਿਕਤਾ",
|
||||
"liveness": "ਜੀਵੰਤਤਾ",
|
||||
"loudness": "ਸ਼ੋਰ",
|
||||
"speechiness": "ਬੋਲਚਾਲਤਾ",
|
||||
"valence": "ਮਨੋਦਯਤਾ",
|
||||
"popularity": "ਲੋਕਪ੍ਰਿਯਤਾ",
|
||||
"key": "ਕੁੰਜੀ",
|
||||
"duration": "ਅਵਧੀ (ਸੈਕਿੰਡ)",
|
||||
"tempo": "ਗਤੀ (BPM)",
|
||||
"mode": "ਮੋਡ",
|
||||
"time_signature": "ਸਮਾਂ ਛਾਪ",
|
||||
"short": "ਸੰਖੇਪ",
|
||||
"medium": "ਮੱਧਮ",
|
||||
"long": "ਲੰਬਾ",
|
||||
"min": "ਨਿਊਨਤਮ",
|
||||
"max": "ਅਧਿਕਤਮ",
|
||||
"target": "ਟੀਚਾ",
|
||||
"moderate": "ਮੱਧਮ",
|
||||
"deselect_all": "ਸਭ ਨੂੰ ਅਣਚੁਣਿਆ ਕਰੋ",
|
||||
"select_all": "ਸਭ ਨੂੰ ਚੁਣੋ",
|
||||
"are_you_sure": "ਕੁ ਤੁਹਾਨੂੰ ਯਕੀਨ ਹੈ?",
|
||||
"generating_playlist": "ਤੁਹਾਡੀ ਆਪਣੀ ਪਲੇਸੂਚੀ ਬਣਾਈ ਜਾ ਰਹੀ ਹੈ...",
|
||||
"selected_count_tracks": "{count} ਟ੍ਰੈਕ ਚੁਣੇ ਗਏ",
|
||||
"download_warning": "ਜੇਕਰ ਤੁਸੀਂ ਸਾਰੇ ਟਰੈਕਾਂ ਨੂੰ ਬਲਕ ਵਿੱਚ ਡਾਊਨਲੋਡ ਕਰਦੇ ਹੋ, ਤਾਂ ਤੁਸੀਂ ਸਪਸ਼ਟ ਤੌਰ 'ਤੇ ਸੰਗੀਤ ਦੀ ਗੈਰ-ਕਾਨੂੰਨੀ ਨਕਲ ਕਰ ਰਹੇ ਹੋ ਅਤੇ ਸੰਗੀਤ ਦੇ ਰਚਨਾਤਮਕ ਸਮਾਜ ਨੂੰ ਨੁਕਸਾਨ ਪਹੁੰਚਾ ਰਹੇ ਹੋ। ਮੈਨੂੰ ਉਮੀਦ ਹੈ ਕਿ ਤੁਸੀਂ ਇਸ ਬਾਰੇ ਜਾਣਦੇ ਹੋ। ਹਮੇਸ਼ਾ ਕਲਾਕਾਰ ਦੀ ਮਿਹਨਤ ਦਾ ਸਤਿਕਾਰ ਅਤੇ ਸਮਰਥਨ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
|
||||
"download_ip_ban_warning": "ਬਾਹਰੀ ਡਾਉਨਲੋਡ ਬੇਨਤੀਆਂ ਕਾਰਨ ਤੁਹਾਡਾ IP YouTube 'ਤੇ ਵੱਧ ਤੋਂ ਵੱਧ ਬਲੌਕ ਹੋ ਸਕਦਾ ਹੈ। IP ਬਲਾਕ ਦਾ ਮਤਲਬ ਹੈ ਕਿ ਤੁਸੀਂ ਘੱਟੋ-ਘੱਟ 2-3 ਮਹੀਨਿਆਂ ਲਈ ਉਸੇ IP ਡਿਵਾਈਸ ਤੋਂ YouTube ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਕਰ ਸਕੋਗੇ (ਭਾਵੇਂ ਤੁਸੀਂ ਲੌਗਇਨ ਕੀਤਾ ਹੋਵੇ)। ਅਤੇ ਜੇਕਰ ਅਜਿਹਾ ਹੁੰਦਾ ਹੈ ਤਾਂ Spottube ਕੋਈ ਜ਼ਿੰਮੇਵਾਰੀ ਨਹੀਂ ਲੈਂਦਾ।",
|
||||
"by_clicking_accept_terms": "'ਸਵੀਕਾਰ' ਕਲਿੱਕ ਕਰਕੇ ਤੁਸੀਂ ਹੇਠਾਂ ਦਿੱਤੀਆਂ ਸ਼ਰਤਾਂ ਨਾਲ ਸਹਿਮਤ ਹੁੰਦੇ ਹੋ:",
|
||||
"download_agreement_1": "ਮੈਨੂੰ ਪਤਾ ਹੈ ਕਿ ਮੈਂ ਗ਼ੈਰ-ਕਾਨੂੰਨੀ ਸੰਗੀਤ ਨੂੰ ਨਕਲੀ ਬਣਾ ਰਿਹਾ ਹਾਂ। ਮੈਂ ਬੁਰਾ ਹਾਂ",
|
||||
"download_agreement_2": "ਮੈਂ ਜਿੱਥੇ ਵੀ ਹੋ ਸਕੇ ਕਲਾਕਾਰਾਂ ਦਾ ਸਮਰਥਨ ਕਰਾਂਗਾ ਅਤੇ ਮੈਂ ਅਜਿਹਾ ਇਸ ਲਈ ਕਰ ਰਿਹਾ ਹਾਂ ਕਿਉਂਕਿ ਮੇਰੇ ਕੋਲ ਉਨ੍ਹਾਂ ਦੀ ਕਲਾ ਖਰੀਦਣ ਲਈ ਪੈਸੇ ਨਹੀਂ ਹਨ।",
|
||||
"download_agreement_3": "ਮੈਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਪਤਾ ਹੈ ਕਿ ਮੇਰਾ ਆਈਪੀ ਯੂਟਿਊਬ 'ਤੇ ਬਲੌਕ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ ਅਤੇ ਮੈਂ ਕਿਸੇ ਵੀ ਦੁਰਘਟਨਾ ਲਈ ਸਪੋਟੀਫਾਈ ਜਾਂ ਇਸਦੇ ਮਾਲਕਾਂ/ਸਬੰਧੀਆਂ ਨੂੰ ਜ਼ਿੰਮੇਵਾਰ ਨਹੀਂ ਠਹਿਰਾਉਂਦਾ ਹਾਂ।",
|
||||
"decline": "ਇਨਕਾਰ ਕਰੋ",
|
||||
"accept": "ਸਵੀਕਾਰ ਕਰੋ",
|
||||
"details": "ਵਿਵਰਣ",
|
||||
"youtube": "ਯੂਟਿਊਬ",
|
||||
"channel": "ਚੈਨਲ",
|
||||
"likes": "ਪਸੰਦ",
|
||||
"dislikes": "ਨਾਪਸੰਦ",
|
||||
"views": "ਵਿਯੂ",
|
||||
"streamUrl": "ਸਟ੍ਰੀਮ ਯੂਆਰਐਲ",
|
||||
"stop": "ਰੋਕੋ",
|
||||
"sort_newest": "ਨਵੇਂ ਜੋੜੇ ਗਏ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
|
||||
"sort_oldest": "ਸਭਤੋਂ ਪੁਰਾਣੇ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
|
||||
"sleep_timer": "ਸਲੀਪ ਟਾਈਮਰ",
|
||||
"mins": "{minutes} ਮਿੰਟ",
|
||||
"hours": "{hours} ਘੰਟੇ",
|
||||
"hour": "{hours} ਘੰਟਾ",
|
||||
"custom_hours": "ਕਸਟਮ ਘੰਟੇ",
|
||||
"logs": "ਚਿੱਠੇ",
|
||||
"developers": "ਡਿਵੈਲਪਰ",
|
||||
"not_logged_in": "ਤੁਸੀਂ ਦਾਖ਼ਲ ਨਹੀਂ ਹੋ",
|
||||
"search_mode": "ਖੋਜ ਮੋਡ",
|
||||
"audio_source": "ਆਡੀਓ ਸਰੋਤ",
|
||||
"ok": "ਠੀਕ ਹੈ",
|
||||
"failed_to_encrypt": "ਇਨਕ੍ਰਿਪਟ ਕਰਨ ਵਿੱਚ ਅਸਫਲ",
|
||||
"encryption_failed_warning": "Spottube ਤੁਹਾਡੇ ਡੇਟਾ ਨੂੰ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਸਟੋਰ ਕਰਨ ਲਈ ਐਨਕ੍ਰਿਪਸ਼ਨ ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਪਰ ਇਹ ਅਸਫਲ ਰਿਹਾ. ਇਸ ਲਈ, ਇਹ ਅਸੁਰੱਖਿਅਤ ਸਟੋਰੇਜ 'ਤੇ ਵਾਪਸ ਆ ਜਾਵੇਗਾ\nਜੇਕਰ ਤੁਸੀਂ ਲੀਨਕਸ ਦੀ ਵਰਤੋਂ ਕਰ ਰਹੇ ਹੋ, ਤਾਂ ਕਿਰਪਾ ਕਰਕੇ ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡੇ ਕੋਲ ਕੋਈ ਗੁਪਤ-ਸੇਵਾਵਾਂ ਜਿਵੇਂ ਕਿ gnome-keyring, kde-wallet, Keepassxc ਆਦਿ ਇੰਸਟਾਲ ਹਨ।",
|
||||
"querying_info": "ਜਾਣਕਾਰੀ ਪ੍ਰਾਪਤ ਕਰ ਰਿਹਾ ਹੈ",
|
||||
"piped_api_down": "ਪਾਈਪਡ ਏਪੀਆਈ ਡਾਊਨ ਹੈ",
|
||||
"piped_down_error_instructions": "ਪਾਈਪਡ ਇੰਸਟੈਂਸ {pipedInstance} ਵਰਤਮਾਨ ਵਿੱਚ ਡਾਊਨ ਹੈ\n\nਇੰਸਟੈਂਸ ਬਦਲੋ ਜਾਂ 'ਏਪੀਆਈ ਪ੍ਰਕਾਰ' ਨੂੰ ਅਧਿਕਾਰਤ YouTube API ਵਿੱਚ ਬਦਲੋ\n\nਤਬਦੀਲੀਆਂ ਤੋਂ ਬਾਅਦ ਐਪ ਨੂੰ ਦੁਬਾਰਾ ਚਲਾਉਣਾ ਯਕੀਨੀ ਬਣਾਓ",
|
||||
"you_are_offline": "ਤੁਸੀਂ ਵਰਤਮਾਨ ਵਿੱਚ ਆਫ਼ਲਾਈਨ ਹੋ",
|
||||
"connection_restored": "ਤੁਹਾਡਾ ਇੰਟਰਨੈੱਟ ਕੁਨੈਕਸ਼ਨ ਬਹਾਲ ਹੋ ਗਿਆ ਹੈ",
|
||||
"use_system_title_bar": "ਸਿਸਟਮ ਸਿਰਲੇਖ ਪੱਟੀ ਵਰਤੋ",
|
||||
"update_playlist": "ਪਲੇਸੂਚੀ ਅੱਪਡੇਟ ਕਰੋ",
|
||||
"update": "ਅੱਪਡੇਟ ਕਰੋ",
|
||||
"crunching_results": "ਨਤੀਜਿਆਂ 'ਤੇ ਕਾਰਵਾਈ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ...",
|
||||
"search_to_get_results": "ਨਤੀਜੇ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਖੋਜੋ",
|
||||
"use_amoled_mode": "AMOLED ਮੋਡ ਵਰਤੋ",
|
||||
"pitch_dark_theme": "ਪਿੱਚ ਬਲੈਕ ਗੂੜ੍ਹੀ ਥੀਮ",
|
||||
"normalize_audio": "ਆਡੀਓ ਨੂੰ ਸਧਾਰਨ ਕਰੋ",
|
||||
"change_cover": "ਕਵਰ ਬਦਲੋ",
|
||||
"add_cover": "ਕਵਰ ਜੋੜੋ",
|
||||
"restore_defaults": "ਡਿਫਾਲਟ ਸੈਟਿੰਗਾਂ ਬਹਾਲ ਕਰੋ",
|
||||
"download_music_codec": "ਸੰਗੀਤ ਕੋਡੈਕ ਡਾਊਨਲੋਡ ਕਰੋ",
|
||||
"streaming_music_codec": "ਸਟ੍ਰੀਮਿੰਗ ਸੰਗੀਤ ਕੋਡੈਕ",
|
||||
"login_with_lastfm": "Last.fm ਨਾਲ ਦਾਖ਼ਲ ਹੋਵੋ",
|
||||
"connect": "ਕਨੈਕਟ ਕਰੋ",
|
||||
"disconnect_lastfm": "Last.fm ਤੋਂ ਡਿਸਕਨੈਕਟ ਕਰੋ",
|
||||
"disconnect": "ਡਿਸਕਨੈਕਟ ਕਰੋ",
|
||||
"username": "ਉਪਯੋਗਕਰਤਾ ਨਾਮ",
|
||||
"password": "ਪਾਸਵਰਡ",
|
||||
"login": "ਦਾਖ਼ਲ ਹੋਵੋ",
|
||||
"login_with_your_lastfm": "ਆਪਣੇ Last.fm ਖ਼ਾਤੇ ਨਾਲ਼ ਦਾਖ਼ਲ ਹੋਵੋ",
|
||||
"scrobble_to_lastfm": "Last.fm ਉੱਤੇ ਸਕਰੌਬਲ ਕਰੋ",
|
||||
"go_to_album": "ਐਲਬਮ ਤੇ ਜਾਓ",
|
||||
"discord_rich_presence": "ਡਿਸਕਾਰਡ ਰਿਚ ਪ੍ਰੈਜੈਂਸ",
|
||||
"browse_all": "ਸਾਰਿਆਂ ਨੂੰ ਬ੍ਰਾਊਜ਼ ਕਰੋ",
|
||||
"genres": "ਸ਼ੈਲੀਆਂ",
|
||||
"explore_genres": "ਸ਼ੈਲੀਆਂ ਫਰੋਲੋ",
|
||||
"step_3_steps": "\"sp_dc\" ਕੁਕੀ ਦਾ ਮੁੱਲ ਕਾਪੀ ਕਰੋ",
|
||||
"step_4_steps": "ਕਾਪੀ ਕੀਤੇ ਗਏ \"sp_dc\" ਮੁੱਲ ਨੂੰ ਪੈਸਟ ਕਰੋ",
|
||||
"friends": "ਦੋਸਤ",
|
||||
"no_lyrics_available": "ਮਾਫ਼ ਕਰਨਾ, ਇਸ ਟ੍ਰੈਕ ਦੇ ਬੋਲ ਨਹੀਂ ਮਿਲ ਸਕੋ",
|
||||
"sort_duration": "ਅਵਧੀ ਅਨੁਸਾਰ ਕ੍ਰਮਬੱਧ ਕਰੋ",
|
||||
"start_a_radio": "ਰੇਡੀਉ ਸ਼ੁਰੂ ਕਰੋ",
|
||||
"how_to_start_radio": "ਰੇਡੀਉ ਕਿਵੇਂ ਸ਼ੁਰੂ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"replace_queue_question": "ਕੀ ਤੁਸੀਂ ਮੌਜੂਦਾ ਕਤਾਰ ਨੂੰ ਬਦਲਣਾ ਚਾਹੁੰਦੇ ਹੋ ਜਾਂ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"endless_playback": "ਬੇਅੰਤ ਪਲੇਬੈਕ",
|
||||
"delete_playlist": "ਪਲੇਸੂਚੀ ਹਟਾਉ",
|
||||
"delete_playlist_confirmation": "ਕੀ ਤੁਸੀਂ ਸੱਚਮੁੱਚ ਇਸ ਪਲੇਸੂਚੀ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"local_tracks": "ਸਥਾਨਕ ਟ੍ਰੈਕ",
|
||||
"song_link": "ਗੀਤ ਦਾ ਲਿੰਕ",
|
||||
"skip_this_nonsense": "ਇਸਨੂੰ ਛੱਡੋ",
|
||||
"freedom_of_music": "“ਸੰਗੀਤ ਦੀ ਅਜ਼ਾਦੀ”",
|
||||
"freedom_of_music_palm": "“ਹੱਥ ਵਿੱਚ ਸੰਗੀਤ ਦੀ ਅਜ਼ਾਦੀ”",
|
||||
"get_started": "ਆਉ ਸ਼ੁਰੂ ਕਰੋ",
|
||||
"youtube_source_description": "ਸਿਫ਼ਾਰਸ਼ ਕੀਤਾ ਗਿਆ ਅਤੇ ਸਭਤੋਂ ਵਧੀਆ ਕੰਮ ਕਰਦਾ ਹੈ।",
|
||||
"piped_source_description": "ਮੁਫ਼ਤ ਮਹਿਸੂਸ ਕਰ ਰਹੇ ਹੋ? YouTube ਦੇ ਸਮਾਨ ਪਰ ਕਾਫੀ ਮੁਫ਼ਤ।",
|
||||
"jiosaavn_source_description": "ਦੱਖਣੀ ਏਸ਼ੀਆਈ ਖੇਤਰ ਲਈ ਸਭ ਤੋਂ ਵਧੀਆ।",
|
||||
"highest_quality": "ਸਭਤੋਂ ਵਧੀਆ ਗੁਣਵੱਤਾ: {quality}",
|
||||
"select_audio_source": "ਆਡੀਉ ਸਰੋਤ ਚੁਣੋ",
|
||||
"endless_playback_description": "ਕ੍ਰਮਬੱਧ ਕਤਾਰ ਦੇ ਅੰਤ ਵਿੱਚ ਆਪਣੇ ਆਪ ਨਵੇਂ ਗੀਤ ਸ਼ਾਮਲ ਕਰੋ",
|
||||
"choose_your_region": "ਆਪਣਾ ਖੇਤਰ ਚੁਣੋ",
|
||||
"choose_your_region_description": "ਇਹ Spotube ਨੂੰ ਤੁਹਾਡੇ ਟਿਕਾਣੇ ਲਈ ਸਹੀ ਸਮੱਗਰੀ ਦਿਖਾਉਣ ਵਿੱਚ ਤੁਹਾਡੀ ਮਦਦ ਕਰੇਗਾ।",
|
||||
"choose_your_language": "ਆਪਣੀ ਭਾਸ਼ਾ ਚੁਣੋ",
|
||||
"help_project_grow": "ਇਸ ਪਰਿਯੋਜਨਾ ਨੂੰ ਵਧਾਉਣ ਵਿੱਚ ਮਦਦ ਕਰੋ",
|
||||
"help_project_grow_description": "Spotube ਇੱਕ ਓਪਨ ਸੋਰਸ ਪ੍ਰੋਜੈਕਟ ਹੈ। ਤੁਸੀਂ ਯੋਗਦਾਨ ਦੇ ਕੇ, ਬੱਗਾਂ ਦੀ ਰਿਪੋਰਟ ਕਰਕੇ, ਜਾਂ ਨਵੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਦਾ ਸੁਝਾਅ ਦੇ ਕੇ ਇਸ ਪ੍ਰੋਜੈਕਟ ਨੂੰ ਵਧਾ ਸਕਦੇ ਹੋ।",
|
||||
"contribute_on_github": "GitHub ਉੱਤੇ ਯੋਗਦਾਨ ਦਿਉ",
|
||||
"donate_on_open_collective": "ਓਪਨ ਕਲੈਕਟਿਵ ਉੱਤੇ ਯੋਗਦਾਨ ਦਿਉ",
|
||||
"browse_anonymously": "ਬਿਨ੍ਹਾ ਨਾਮ ਦੇ ਬ੍ਰਾਊਜ਼ ਕਰੋ",
|
||||
"enable_connect": "ਕਨੈਕਟ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ",
|
||||
"enable_connect_description": "ਹੋਰ ਡਿਵਾਈਸਾਂ ਤੋਂ Spottube ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ",
|
||||
"devices": "ਉਪਕਰਣ",
|
||||
"select": "ਚੁਣੋ",
|
||||
"connect_client_alert": "ਤੁਹਾਨੂੰ {client} ਦੁਆਰਾ ਕੰਟਰੋਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
|
||||
"this_device": "ਇਹ ਉਪਕਰਣ",
|
||||
"remote": "ਰਿਮੋਟ",
|
||||
"local_library": "ਸਥਾਨਕ ਲਾਇਬ੍ਰੇਰੀ",
|
||||
"add_library_location": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਜੋੜੋ",
|
||||
"remove_library_location": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ ਹਟਾਉ",
|
||||
"local_tab": "ਸਥਾਨਕ",
|
||||
"stats": "ਅੰਕੜੇ",
|
||||
"and_n_more": "और {count} और",
|
||||
"recently_played": "ਹਾਲ ਹੀ ਵਿੱਚ ਚਲਾਏ ਗਏ",
|
||||
"browse_more": "ਹੋਰ ਬ੍ਰਾਉਜ਼ ਕਰੋ",
|
||||
"no_title": "ਕੋਈ ਸਿਰਲੇਖ ਨਹੀਂ",
|
||||
"not_playing": "ਚੱਲ ਨਹੀਂ ਰਿਹਾ",
|
||||
"epic_failure": "ਮਹਾਨ ਅਸਫ਼ਲਤਾ!",
|
||||
"added_num_tracks_to_queue": "{tracks_length} ਟ੍ਰੈਕ ਕਤਾਰ ਵਿੱਚ ਜੋੜੇ ਗਏ",
|
||||
"spotube_has_an_update": "Spotube ਲਈ ਇੱਕ ਅੱਪਡੇਟ ਹੈ",
|
||||
"download_now": "ਹੁਣੇ ਡਾਊਨਲੋਡ ਕਰੋ",
|
||||
"nightly_version": "Spotube Nightly {nightlyBuildNum} ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"release_version": "Spotube v{version} ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ",
|
||||
"read_the_latest": "ਨਵੀਨਤਮ ਪੜ੍ਹੋ",
|
||||
"release_notes": "ਰਿਲੀਜ਼ ਨੋਟਸ",
|
||||
"pick_color_scheme": "ਰੰਗ ਯੋਜਨਾ ਚੁਣੋ",
|
||||
"save": "ਸੰਭਾਲੋ",
|
||||
"choose_the_device": "ਉਪਕਰਣ ਚੁਣੋ:",
|
||||
"multiple_device_connected": "ਕਈ ਉਪਕਰਣ ਜੋੜੇ ਗਏ ਹਨ।\nਉਸ ਉਪਕਰਣ ਨੂੰ ਚੁਣੋ ਜਿਸ ਤੇ ਤੁਸੀਂ ਕਿਰਿਆ ਕਰਨੀ ਚਾਹੁੰਦੇ ਹੋ",
|
||||
"nothing_found": "ਕੁੱਝ ਨਹੀਂ ਮਿਲਿਆ",
|
||||
"the_box_is_empty": "ਡੱਬਾ ਖਾਲੀ ਹੈ",
|
||||
"top_artists": "ਚੋਟੀ ਦਾ ਕਲਾਕਾਰ",
|
||||
"top_albums": "ਚੋਟੀ ਦੀ ਐਲਬਮ",
|
||||
"this_week": "ਇਸ ਹਫ਼ਤੇ",
|
||||
"this_month": "ਇਸ ਮਹੀਨੇ",
|
||||
"last_6_months": "ਪਿਛਲੇ 6 ਮਹੀਨੇ",
|
||||
"this_year": "ਇਸ ਸਾਲ",
|
||||
"last_2_years": "ਪਿਛਲੇ 2 साल",
|
||||
"all_time": "ਪੂਰਾ ਸਮਾਂ",
|
||||
"powered_by_provider": "{providerName} ਦੁਆਰਾ ਸੰਚਾਲਿਤ",
|
||||
"email": "ਈਮੇਲ",
|
||||
"profile_followers": "ਫ਼ਾਲੋਅਰ",
|
||||
"birthday": "ਜਨਮਦਿਨ",
|
||||
"subscription": "ਗਾਹਕੀਆਂ",
|
||||
"not_born": "ਹਲੇ ਪੈਦਾ ਨਹੀਂ ਹੋਇਆ",
|
||||
"hacker": "ਹੈਕਰ",
|
||||
"profile": "ਪ੍ਰੋਫ਼ਾਈਲ",
|
||||
"no_name": "ਕੋਈ ਨਾਮ ਨਹੀਂ",
|
||||
"edit": "ਸੋਧੋ",
|
||||
"user_profile": "ਵਰਤੋਂਕਾਰ ਪ੍ਰੋਫ਼ਾਈਲ",
|
||||
"count_plays": "{count} ਪਲੇ",
|
||||
"streaming_fees_hypothetical": "ਸਟ੍ਰੀਮਿੰਗ ਫੀਸ (ਕਾਲਪਨਿਕ)",
|
||||
"count_mins": "{minutes} ਮਿੰਟ",
|
||||
"summary_minutes": "ਮਿੰਟ",
|
||||
"summary_listened_to_music": "ਸੁਣਿਆ ਗਿਆ ਸੰਗੀਤ",
|
||||
"summary_songs": "ਗੀਤ",
|
||||
"summary_streamed_overall": "ਕੁੱਲ ਸਟ੍ਰੀਮ",
|
||||
"summary_owed_to_artists": "ਕਲਾਕਾਰਾਂ ਨੂੰ ਦੇਣਦਾਰ\nਇਸ ਮਹੀਨੇ",
|
||||
"summary_artists": "ਕਲਾਕਾਰ",
|
||||
"summary_music_reached_you": "ਸੰਗੀਤ ਤੁਹਾਡੇ ਤੱਕ ਪਹੁੰਚ ਗਿਆ",
|
||||
"summary_full_albums": "ਪੂਰਾ ਐਲਬਮ",
|
||||
"summary_got_your_love": "ਤੁਹਾਡਾ ਪਿਆਰ ਮਿਲਿਆ",
|
||||
"summary_playlists": "ਪਲੇਸੂਚੀਆਂ",
|
||||
"summary_were_on_repeat": "ਦੁਹਰਾਇਆ ਗਿਆ",
|
||||
"total_money": "ਕੁੱਲ {money}",
|
||||
"minutes_listened": "ਮਿੰਟ ਸੁਣਿਆ",
|
||||
"streamed_songs": "ਗੀਤ ਸਟ੍ਰੀਮ ਕੀਤੇ",
|
||||
"count_streams": "{count} ਸਟ੍ਰੀਮ",
|
||||
"owned_by_you": "ਤੁਹਾਡੀ ਮਲਕੀਅਤ",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} ਕਲਿੱਪਬੋਰਡ ਉੱਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ",
|
||||
"spotify_hipotetical_calculation": "*ਇਸਦੀ ਗਣਨਾ Spotify ਪ੍ਰਤੀ ਸਟ੍ਰੀਮ\n$0.003 ਤੋਂ $0.005 ਦੇ ਭੁਗਤਾਨ ਦੇ ਆਧਾਰ 'ਤੇ ਕੀਤੀ ਜਾਂਦੀ ਹੈ। ਇਹ ਇੱਕ ਕਾਲਪਨਿਕ\nਗਣਨਾ ਹੈ ਜੋ ਉਪਭੋਗਤਾ ਨੂੰ ਇਸ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦਿੰਦਾ ਹੈ ਕਿ \nਜੇਕਰ ਉਹਨਾਂ ਨੇ Spotify 'ਤੇ ਗੀਤ ਸੁਣਿਆ ਤਾਂ ਉਹਨਾਂ ਨੇ ਕਿੰਨਾ ਭੁਗਤਾਨ ਕੀਤਾ ਹੋਵੇਗਾ।",
|
||||
"webview_not_found": "ਵੈੱਬਵਿਯੂ ਨਹੀਂ ਮਿਲਿਆ",
|
||||
"webview_not_found_description": "ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਉੱਤੇ ਕੋਈ ਵੈੱਬਵਿਯੂ ਰਨਟਾਈਮ ਇੰਸਟਾਲ ਨਹੀਂ ਹੈ।\nਜੈਕਰ ਇਹ ਇੰਸਟਾਲ ਹੈ ਤਾਂ ਪੱਕਾ ਕਰੋ ਕਿ ਇਹ ਵਾਤਾਵਰਨ ਰਸਤੇ ਵਿੱਚ ਹੈ\n\nਇੰਸਟਾਲ ਕਰਨ ਤੋਂ ਬਾਅਦ, ਐਪ ਦੁਬਾਰਾ ਚਲਾਉ",
|
||||
"unsupported_platform": "ਅਸਮਰਥਿਤ ਪਲੇਟਫਾਰਮ",
|
||||
"invidious_instance": "ਇਨਡਿਵੀਡਿਊਸ ਸਰਵਰ ਇੰਸਟੈਂਸ",
|
||||
"invidious_description": "ਟ੍ਰੈਕ ਮਿਲਾਨ ਲਈ ਇਨਡਿਵੀਡਿਊਸ ਸਰਵਰ ਇੰਸਟੈਂਸ",
|
||||
"invidious_warning": "ਕੁੱਝ ਇੰਸਟੈਂਸ ਸਹੀ ਕੰਮ ਨਹੀਂ ਕਰ ਰਹੇ ਹਨ। ਆਪਣੇ ਜੋਖਮ ਤੇ ਵਰਤੋ",
|
||||
"invidious_source_description": "ਪਾਈਪਡ ਦੇ ਸਮਾਨ, ਪਰ ਵੱਧ ਉਪਲਬਧਤਾ ਨਾਲ",
|
||||
"cache_music": "ਸੰਗੀਤ ਨੂੰ ਕੈਸ਼ ਕਰੋ",
|
||||
"open": "ਖੋਲ੍ਹੋ",
|
||||
"cache_folder": "ਕੈਸ਼ ਫੋਲਡਰ",
|
||||
"export": "ਨਿਰਯਾਤ ਕਰੋ",
|
||||
"clear_cache": "ਕੈਸ਼ ਸਾਫ਼ ਕਰੋ",
|
||||
"clear_cache_confirmation": "ਕੀ ਤੁਸੀਂ ਕੈਸ਼ ਸਾਫ਼ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"export_cache_files": "ਕੈਸ਼ ਫ਼ਾਈਲਾਂ ਨਿਰਯਾਤ ਕਰੋ",
|
||||
"found_n_files": "{count} ਫ਼ਾਈਲਾਂ ਮਿਲੀਆਂ",
|
||||
"export_cache_confirmation": "ਕੀ ਤੁਸੀਂ ਇਹਨਾਂ ਫ਼ਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ",
|
||||
"exported_n_out_of_m_files": "{files} ਵਿੱਚੋਂ {filesExported} ਫ਼ਾਈਲਾਂ ਨਿਰਯਾਤ ਕੀਤੀਆਂ ਗਈਆਂ"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user