Compare commits

...

5 Commits

Author SHA1 Message Date
Kingkor Roy Tirtho
bba725a139 fix(android): fully close the app on swipe-away and avoid duplicate cookie DataStore
Initialize NewPipeDownloader once per process so recreating the Activity
no longer opens a second DataStore on spotube_cookies.preferences_pb.
Kill the process after PlaybackService shuts down on task removal so the
proxy server, MQTT client and singletons don't linger in the background.
2026-10-30 13:35:39 +06:00
Kingkor Roy Tirtho
c058074cff fix(plugin): update default selected plugins to use built-in plugins with distinct abilities 2026-10-30 13:08:34 +06:00
Kingkor Roy Tirtho
2aaa0db81e fix(broker): update default broker settings and improve connection error handling 2026-10-30 11:50:39 +06:00
Kingkor Roy Tirtho
14bc279653 fix(ios): crashing on search screen 2026-10-30 11:10:25 +06:00
Kingkor Roy Tirtho
2c1148a126 chore: remove devil's name 2026-10-30 10:33:11 +06:00
32 changed files with 136 additions and 99 deletions

View File

@ -290,8 +290,8 @@
- **desktop**: double titlebar in local library folders and massive space in
overlay player
- lastfm form broken in other locales #2447
- spotify login broken due to new totp requirement #2494
- spotify authentication 429 errors
- Greedy Green Corp login broken due to new totp requirement #2494
- Greedy Green Corp authentication 429 errors
### Features
@ -326,7 +326,7 @@
- **android**: home widget support (#2148)
- rewrite entire app in shadcn-ui replacing material eww
- grid/list customizable playbutton view
- flag to hide spotify generated images with patterns
- flag to hide Greedy Green Corp generated images with patterns
- show placeholder images where there is no item or on empty page
- pause playback when no internet connection
- implement yt-dlp for desktop and NewPipeExtractor for Android (#2316)
@ -552,7 +552,7 @@
[#1463](https://github.com/team-spotube/spotube/issues/1463)
[#1505](https://github.com/team-spotube/spotube/issues/1505)
([d2683c5](https://github.com/team-spotube/spotube/commit/d2683c52d81d807be6ff72f15b8e9eb18181e211))
- spotify friends and user profile icon (mobile) showing when not authenticated
- Greedy Green Corp friends and user profile icon (mobile) showing when not authenticated
[#1410](https://github.com/team-spotube/spotube/issues/1410)
([9bccbc9](https://github.com/team-spotube/spotube/commit/9bccbc93c63dd34f6e15ff68c276976ecd1d9a33))
- **updater:** dead link
@ -576,7 +576,7 @@
### Features
- add Spotify homepage personalized recommendations
- add Greedy Green Corp homepage personalized recommendations
([#1402](https://github.com/team-spotube/spotube/issues/1402))
([9e25c74](https://github.com/team-spotube/spotube/commit/9e25c742d4e43e4e10d2b48afb8e6d90288ffa11))
- add user profile page
@ -684,7 +684,7 @@
- add create playlist button in add playlist dialog
([2168a64](https://personal.github.com/team-spotube/spotube/commit/2168a640af3104a43139c303d78e2c2326a1bda7))
- add spotify friends activity
- add Greedy Green Corp friends activity
([#1130](https://personal.github.com/team-spotube/spotube/issues/1130))
([7983932](https://personal.github.com/team-spotube/spotube/commit/79839329b0970acccb0c566a31eee508adbc8557))
- **deep-link:** add track opening page
@ -1436,7 +1436,7 @@
([448c9b3](https://github.com/team-spotube/spotube/commit/448c9b39f407668ad92a695afe3c9741baeca20d))
- **macos:** crashing on startup
([c46b428](https://github.com/team-spotube/spotube/commit/c46b4284b1d46a614cbcebc8c2f2e52714921b9b))
- spotify query hooks overriding default query params
- Greedy Green Corp query hooks overriding default query params
([ec9a02e](https://github.com/team-spotube/spotube/commit/ec9a02e8b8d988e15ed58027054d2a9090d98873))
## [2.7.0](https://github.com/team-spotube/spotube/compare/v2.6.0...v2.7.0) (2023-03-07)
@ -1469,7 +1469,7 @@
([9080441](https://github.com/team-spotube/spotube/commit/9080441b875ceb91260bbad79291365a98d5be95))
- individual shuffle and repeat/loop button of player
([f79223c](https://github.com/team-spotube/spotube/commit/f79223cd41c61d9836d25e7bc2811c6515ba00c8))
- **lyrics:** use official spotify API for fetching lyrics and add zoom controls
- **lyrics:** use official Greedy Green Corp API for fetching lyrics and add zoom controls
([10d0660](https://github.com/team-spotube/spotube/commit/10d0660972f008df0d11c280b681ce3b78f05d0b))
- **mobile:** pull to refresh support in all refreshable list views
([9f959ce](https://github.com/team-spotube/spotube/commit/9f959ce77cd95cfc34d01af1f5cf53dd4206b6a6))
@ -1921,7 +1921,7 @@
- private playlists of current user aren't shown fix
(https://github.com/team-spotube/spotube/issues/92)
- refresh token error causing re-login (culprit: internal lib spotify-dart)
- refresh token error causing re-login (culprit: internal lib Greedy Green Corp-dart)
- Typo in Login instructions URL
# v2.1.0
@ -1937,7 +1937,7 @@
### Improved
- Spotify track to youtube track algorithm
- Greedy Green Corp track to youtube track algorithm
- Genius lyrics matching algorithm
- Download track. Checks if already exists & replaces on user command
- Wide screen responsiveness & adaptation
@ -1950,7 +1950,7 @@
(https://github.com/team-spotube/spotube/issues/46)
- Theme modification state doesn't persist
(https://github.com/team-spotube/spotube/issues/54)
- Wrong URI path for "Login with Spotify" tutorial
- Wrong URI path for "Login with Greedy Green Corp" tutorial
(https://github.com/team-spotube/spotube/issues/69)
- Card shadow showing in the background of TitleBar & Searchbar
@ -2124,17 +2124,17 @@
### Bug Fixes
- `access_token not found` Error after OAuth Login with Spotify credentials
- `access_token not found` Error after OAuth Login with Greedy Green Corp credentials
(used to need a restart of the app to load the access_token)
- `access_token not found` Error after OAuth Login with Spotify credentials
- `access_token not found` Error after OAuth Login with Greedy Green Corp credentials
(used to need a restart of the app to load the access_token)
- Volume level wasn't cached even after changing volume
# v0.0.1
Spotube v0.0.1 - initial release of the open source software for playing Spotify
Spotube v0.0.1 - initial release of the open source software for playing Greedy Green Corp
music using Youtube public API
Spotube v0.0.1 - initial release of the open source software for playing Spotify
Spotube v0.0.1 - initial release of the open source software for playing Greedy Green Corp
music using Youtube public API
### New
@ -2143,7 +2143,7 @@ music using Youtube public API
- Playback Queue
- Save to Liked Tracks/Playlists
- Bypass API rate limitation on basic usage using personal developer Apps for
spotify API
Greedy Green Corp API
- Bypass API rate limitation on basic usage using personal developer Apps for
spotify API
Greedy Green Corp API
- Youtube search & get handled using scrape-yt

View File

@ -82,10 +82,16 @@ class NewPipeDownloader(private val cookieJar: PersistentCookieJar) : Downloader
companion object {
private var instance: NewPipeDownloader? = null
/**
* Safe to call on every Activity creation: the process (and so this singleton) outlives
* the Activity, and a second [PersistentCookieJar] would open a second DataStore on the
* same cookies file, which DataStore rejects.
*/
@Synchronized
fun init(paths: Paths) {
if (instance != null) return
val cookieJar = PersistentCookieJar(paths)
instance = NewPipeDownloader(cookieJar)
NewPipe.init(instance)
instance = NewPipeDownloader(cookieJar).also { NewPipe.init(it) }
}
}
}

View File

@ -23,6 +23,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.Process
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat
@ -61,6 +62,8 @@ import spotube.composeapp.generated.resources.notification_playback_channel_name
@OptIn(UnstableApi::class)
class PlaybackService : MediaLibraryService(), KoinComponent {
private var taskRemoved = false
private val audioPlayer = inject<AudioPlayerInterface>().value as AudioPlayer
private val audioPlayerQueue: AudioPlayerQueue by inject()
@ -161,9 +164,13 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
removeSession(librarySession)
librarySession.release()
super.onDestroy()
// Swiping the app out of recents should fully close it: Android otherwise keeps this
// process cached with the proxy server, MQTT client and every singleton still alive.
if (taskRemoved) Process.killProcess(Process.myPid())
}
override fun onTaskRemoved(rootIntent: Intent?) {
taskRemoved = true
pauseAllPlayersAndStopSelf()
}

View File

@ -402,7 +402,7 @@
<string name="welcome_guide_title">كيفية استخدام Spotube</string>
<string name="welcome_guide_subtitle">أربع خطوات سريعة وتبدأ الاستماع.</string>
<string name="welcome_guide_signin_title">تسجيل الدخول</string>
<string name="welcome_guide_signin_desc">اربط حسابك على Spotify، أو اختر إضافة الكتالوج الخاصة بك.</string>
<string name="welcome_guide_signin_desc">اربط حسابك، أو اختر إضافة الكتالوج الخاصة بك.</string>
<string name="welcome_guide_search_title">ابحث وشغّل</string>
<string name="welcome_guide_search_desc">اعثر على أي أغنية أو ألبوم أو فنان، ثم اضغط تشغيل.</string>
<string name="welcome_guide_offline_title">استمع بلا إنترنت</string>

View File

@ -386,7 +386,7 @@
<string name="welcome_guide_title">Spotube যেভাবে ব্যবহার করবেন</string>
<string name="welcome_guide_subtitle">চারটি সহজ ধাপ, তারপরই গান শুনুন।</string>
<string name="welcome_guide_signin_title">সাইন ইন</string>
<string name="welcome_guide_signin_desc">আপনার Spotify অ্যাকাউন্ট কানেক্ট করুন, অথবা নিজের পছন্দের ক্যাটালগ প্লাগইন বেছে নিন।</string>
<string name="welcome_guide_signin_desc">আপনার অ্যাকাউন্ট কানেক্ট করুন, অথবা নিজের পছন্দের ক্যাটালগ প্লাগইন বেছে নিন।</string>
<string name="welcome_guide_search_title">খুঁজুন ও প্লে করুন</string>
<string name="welcome_guide_search_desc">যেকোনো গান, অ্যালবাম বা শিল্পী খুঁজুন, তারপর প্লে চাপুন।</string>
<string name="welcome_guide_offline_title">অফলাইনে শুনুন</string>

View File

@ -386,7 +386,7 @@
<string name="welcome_guide_title">So nutzt du Spotube</string>
<string name="welcome_guide_subtitle">Vier schnelle Schritte und du hörst Musik.</string>
<string name="welcome_guide_signin_title">Anmelden</string>
<string name="welcome_guide_signin_desc">Verbinde dein Spotify-Konto oder wähle ein eigenes Katalog-Plugin.</string>
<string name="welcome_guide_signin_desc">Verbinde dein Konto oder wähle ein eigenes Katalog-Plugin.</string>
<string name="welcome_guide_search_title">Suchen und abspielen</string>
<string name="welcome_guide_search_desc">Finde einen Song, ein Album oder einen Künstler und tippe auf Abspielen.</string>
<string name="welcome_guide_offline_title">Offline hören</string>

View File

@ -390,7 +390,7 @@
<string name="welcome_guide_title">Cómo usar Spotube</string>
<string name="welcome_guide_subtitle">Cuatro pasos rápidos y a escuchar.</string>
<string name="welcome_guide_signin_title">Inicia sesión</string>
<string name="welcome_guide_signin_desc">Conecta tu cuenta de Spotify o elige tu propio plugin de catálogo.</string>
<string name="welcome_guide_signin_desc">Conecta tu cuenta o elige tu propio plugin de catálogo.</string>
<string name="welcome_guide_search_title">Busca y reproduce</string>
<string name="welcome_guide_search_desc">Encuentra cualquier canción, álbum o artista y dale al play.</string>
<string name="welcome_guide_offline_title">Escucha sin conexión</string>

View File

@ -386,7 +386,7 @@
<string name="welcome_guide_title">نحوهٔ استفاده از Spotube</string>
<string name="welcome_guide_subtitle">چهار گام سریع و مشغول شنیدن هستید.</string>
<string name="welcome_guide_signin_title">ورود</string>
<string name="welcome_guide_signin_desc">حساب Spotify خود را متصل کنید یا افزونهٔ کاتالوگ دلخواهتان را انتخاب کنید.</string>
<string name="welcome_guide_signin_desc">حساب خود را متصل کنید یا افزونهٔ کاتالوگ دلخواهتان را انتخاب کنید.</string>
<string name="welcome_guide_search_title">جستجو و پخش</string>
<string name="welcome_guide_search_desc">هر آهنگ، آلبوم یا هنرمندی را پیدا کنید و پخش را بزنید.</string>
<string name="welcome_guide_offline_title">گوش دادن آفلاین</string>

View File

@ -390,7 +390,7 @@
<string name="welcome_guide_title">Comment utiliser Spotube</string>
<string name="welcome_guide_subtitle">Quatre étapes rapides et c\'est parti.</string>
<string name="welcome_guide_signin_title">Se connecter</string>
<string name="welcome_guide_signin_desc">Connectez votre compte Spotify ou choisissez votre propre plugin de catalogue.</string>
<string name="welcome_guide_signin_desc">Connectez votre compte ou choisissez votre propre plugin de catalogue.</string>
<string name="welcome_guide_search_title">Rechercher et écouter</string>
<string name="welcome_guide_search_desc">Trouvez n\'importe quel titre, album ou artiste, puis lancez la lecture.</string>
<string name="welcome_guide_offline_title">Écouter hors connexion</string>

View File

@ -385,7 +385,7 @@
<string name="welcome_guide_title">Spotube कैसे इस्तेमाल करें</string>
<string name="welcome_guide_subtitle">चार आसान स्टेप और आप सुनना शुरू कर सकते हैं।</string>
<string name="welcome_guide_signin_title">साइन इन करें</string>
<string name="welcome_guide_signin_desc">अपना Spotify अकाउंट कनेक्ट करें, या अपना कैटलॉग प्लगइन चुनें।</string>
<string name="welcome_guide_signin_desc">अपना अकाउंट कनेक्ट करें, या अपना कैटलॉग प्लगइन चुनें।</string>
<string name="welcome_guide_search_title">खोजें और चलाएं</string>
<string name="welcome_guide_search_desc">कोई भी गाना, एल्बम या कलाकार खोजें, फिर प्ले दबाएं।</string>
<string name="welcome_guide_offline_title">ऑफ़लाइन सुनें</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">Cara menggunakan Spotube</string>
<string name="welcome_guide_subtitle">Empat langkah singkat dan kamu siap mendengarkan.</string>
<string name="welcome_guide_signin_title">Masuk</string>
<string name="welcome_guide_signin_desc">Hubungkan akun Spotify-mu, atau pilih plugin katalogmu sendiri.</string>
<string name="welcome_guide_signin_desc">Hubungkan akunmu, atau pilih plugin katalogmu sendiri.</string>
<string name="welcome_guide_search_title">Cari dan putar</string>
<string name="welcome_guide_search_desc">Temukan lagu, album, atau artis apa pun, lalu tekan putar.</string>
<string name="welcome_guide_offline_title">Dengarkan offline</string>

View File

@ -390,7 +390,7 @@
<string name="welcome_guide_title">Come usare Spotube</string>
<string name="welcome_guide_subtitle">Quattro rapidi passaggi e sei pronto ad ascoltare.</string>
<string name="welcome_guide_signin_title">Accedi</string>
<string name="welcome_guide_signin_desc">Collega il tuo account Spotify o scegli il tuo plugin di catalogo.</string>
<string name="welcome_guide_signin_desc">Collega il tuo account o scegli il tuo plugin di catalogo.</string>
<string name="welcome_guide_search_title">Cerca e riproduci</string>
<string name="welcome_guide_search_desc">Trova qualsiasi brano, album o artista, poi premi play.</string>
<string name="welcome_guide_offline_title">Ascolta offline</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">Spotubeの使い方</string>
<string name="welcome_guide_subtitle">簡単な4ステップで、すぐに聴けます。</string>
<string name="welcome_guide_signin_title">サインイン</string>
<string name="welcome_guide_signin_desc">Spotifyアカウントを連携するか、お好みのカタログプラグインを選んでください。</string>
<string name="welcome_guide_signin_desc">アカウントを連携するか、お好みのカタログプラグインを選んでください。</string>
<string name="welcome_guide_search_title">検索して再生</string>
<string name="welcome_guide_search_desc">曲、アルバム、アーティストを探して再生しましょう。</string>
<string name="welcome_guide_offline_title">オフラインで聴く</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">Spotube 사용 방법</string>
<string name="welcome_guide_subtitle">간단한 네 단계면 바로 감상할 수 있어요.</string>
<string name="welcome_guide_signin_title">로그인</string>
<string name="welcome_guide_signin_desc">Spotify 계정을 연결하거나 원하는 카탈로그 플러그인을 선택하세요.</string>
<string name="welcome_guide_signin_desc">계정을 연결하거나 원하는 카탈로그 플러그인을 선택하세요.</string>
<string name="welcome_guide_search_title">검색하고 재생하기</string>
<string name="welcome_guide_search_desc">노래, 앨범, 아티스트를 찾은 다음 재생을 누르세요.</string>
<string name="welcome_guide_offline_title">오프라인으로 감상하기</string>

View File

@ -386,7 +386,7 @@
<string name="welcome_guide_title">Zo gebruik je Spotube</string>
<string name="welcome_guide_subtitle">Vier snelle stappen en je luistert al.</string>
<string name="welcome_guide_signin_title">Inloggen</string>
<string name="welcome_guide_signin_desc">Koppel je Spotify-account of kies je eigen catalogusplug-in.</string>
<string name="welcome_guide_signin_desc">Koppel je account of kies je eigen catalogusplug-in.</string>
<string name="welcome_guide_search_title">Zoeken en afspelen</string>
<string name="welcome_guide_search_desc">Vind elk nummer, album of elke artiest en druk op afspelen.</string>
<string name="welcome_guide_offline_title">Offline luisteren</string>

View File

@ -394,7 +394,7 @@
<string name="welcome_guide_title">Jak korzystać ze Spotube</string>
<string name="welcome_guide_subtitle">Cztery szybkie kroki i już słuchasz.</string>
<string name="welcome_guide_signin_title">Zaloguj się</string>
<string name="welcome_guide_signin_desc">Połącz konto Spotify lub wybierz własną wtyczkę katalogu.</string>
<string name="welcome_guide_signin_desc">Połącz konto lub wybierz własną wtyczkę katalogu.</string>
<string name="welcome_guide_search_title">Szukaj i odtwarzaj</string>
<string name="welcome_guide_search_desc">Znajdź dowolny utwór, album lub wykonawcę i naciśnij Odtwórz.</string>
<string name="welcome_guide_offline_title">Słuchaj offline</string>

View File

@ -390,7 +390,7 @@
<string name="welcome_guide_title">Como usar o Spotube</string>
<string name="welcome_guide_subtitle">Quatro passos rápidos e você já está ouvindo.</string>
<string name="welcome_guide_signin_title">Entrar</string>
<string name="welcome_guide_signin_desc">Conecte sua conta do Spotify ou escolha seu próprio plugin de catálogo.</string>
<string name="welcome_guide_signin_desc">Conecte sua conta ou escolha seu próprio plugin de catálogo.</string>
<string name="welcome_guide_search_title">Busque e toque</string>
<string name="welcome_guide_search_desc">Encontre qualquer música, álbum ou artista e aperte o play.</string>
<string name="welcome_guide_offline_title">Ouça offline</string>

View File

@ -394,7 +394,7 @@
<string name="welcome_guide_title">Как пользоваться Spotube</string>
<string name="welcome_guide_subtitle">Четыре простых шага — и музыка уже играет.</string>
<string name="welcome_guide_signin_title">Вход</string>
<string name="welcome_guide_signin_desc">Подключите аккаунт Spotify или выберите свой плагин каталога.</string>
<string name="welcome_guide_signin_desc">Подключите аккаунт или выберите свой плагин каталога.</string>
<string name="welcome_guide_search_title">Ищите и слушайте</string>
<string name="welcome_guide_search_desc">Найдите любой трек, альбом или исполнителя и нажмите «Воспроизвести».</string>
<string name="welcome_guide_offline_title">Слушайте офлайн</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">วิธีใช้ Spotube</string>
<string name="welcome_guide_subtitle">แค่ 4 ขั้นตอนง่าย ๆ ก็เริ่มฟังได้เลย</string>
<string name="welcome_guide_signin_title">เข้าสู่ระบบ</string>
<string name="welcome_guide_signin_desc">เชื่อมต่อบัญชี Spotify ของคุณ หรือเลือกปลั๊กอินแคตตาล็อกของคุณเอง</string>
<string name="welcome_guide_signin_desc">เชื่อมต่อบัญชีของคุณ หรือเลือกปลั๊กอินแคตตาล็อกของคุณเอง</string>
<string name="welcome_guide_search_title">ค้นหาและเล่น</string>
<string name="welcome_guide_search_desc">ค้นหาเพลง อัลบั้ม หรือศิลปิน แล้วกดเล่น</string>
<string name="welcome_guide_offline_title">ฟังแบบออฟไลน์</string>

View File

@ -386,7 +386,7 @@
<string name="welcome_guide_title">Spotube nasıl kullanılır</string>
<string name="welcome_guide_subtitle">Dört hızlı adım ve dinlemeye başlıyorsun.</string>
<string name="welcome_guide_signin_title">Giriş yap</string>
<string name="welcome_guide_signin_desc">Spotify hesabını bağla veya kendi katalog eklentini seç.</string>
<string name="welcome_guide_signin_desc">hesabını bağla veya kendi katalog eklentini seç.</string>
<string name="welcome_guide_search_title">Ara ve çal</string>
<string name="welcome_guide_search_desc">Herhangi bir şarkıyı, albümü veya sanatçıyı bul, sonra çal\'a bas.</string>
<string name="welcome_guide_offline_title">Çevrimdışı dinle</string>

View File

@ -394,7 +394,7 @@
<string name="welcome_guide_title">Як користуватися Spotube</string>
<string name="welcome_guide_subtitle">Чотири прості кроки — і музика вже грає.</string>
<string name="welcome_guide_signin_title">Вхід</string>
<string name="welcome_guide_signin_desc">Підключіть акаунт Spotify або виберіть власний плагін каталогу.</string>
<string name="welcome_guide_signin_desc">Підключіть акаунт або виберіть власний плагін каталогу.</string>
<string name="welcome_guide_search_title">Шукайте й слухайте</string>
<string name="welcome_guide_search_desc">Знайдіть будь-який трек, альбом чи виконавця й натисніть «Відтворити».</string>
<string name="welcome_guide_offline_title">Слухайте офлайн</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">Cách dùng Spotube</string>
<string name="welcome_guide_subtitle">Bốn bước nhanh là bạn đã có thể nghe nhạc.</string>
<string name="welcome_guide_signin_title">Đăng nhập</string>
<string name="welcome_guide_signin_desc">Kết nối tài khoản Spotify hoặc chọn plugin danh mục của riêng bạn.</string>
<string name="welcome_guide_signin_desc">Kết nối tài khoản hoặc chọn plugin danh mục của riêng bạn.</string>
<string name="welcome_guide_search_title">Tìm kiếm và phát</string>
<string name="welcome_guide_search_desc">Tìm bất kỳ bài hát, album hay nghệ sĩ nào rồi nhấn phát.</string>
<string name="welcome_guide_offline_title">Nghe ngoại tuyến</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">如何使用 Spotube</string>
<string name="welcome_guide_subtitle">只要四個簡單步驟,就能開始聆聽。</string>
<string name="welcome_guide_signin_title">登入</string>
<string name="welcome_guide_signin_desc">連結你的 Spotify 帳戶,或選擇你自己的目錄外掛程式。</string>
<string name="welcome_guide_signin_desc">連結你的帳戶,或選擇你自己的目錄外掛程式。</string>
<string name="welcome_guide_search_title">搜尋並播放</string>
<string name="welcome_guide_search_desc">找到任何歌曲、專輯或藝人,然後按下播放。</string>
<string name="welcome_guide_offline_title">離線聆聽</string>

View File

@ -382,7 +382,7 @@
<string name="welcome_guide_title">如何使用 Spotube</string>
<string name="welcome_guide_subtitle">只需四个简单步骤,即可开始聆听。</string>
<string name="welcome_guide_signin_title">登录</string>
<string name="welcome_guide_signin_desc">连接你的 Spotify 帐号,或选择你自己的曲库插件。</string>
<string name="welcome_guide_signin_desc">连接你的帐号,或选择你自己的曲库插件。</string>
<string name="welcome_guide_search_title">搜索并播放</string>
<string name="welcome_guide_search_desc">查找任意歌曲、专辑或艺人,然后点击播放。</string>
<string name="welcome_guide_offline_title">离线收听</string>

View File

@ -404,7 +404,7 @@
<string name="welcome_guide_title">How to use Spotube</string>
<string name="welcome_guide_subtitle">Four quick steps and you’re listening.</string>
<string name="welcome_guide_signin_title">Sign in</string>
<string name="welcome_guide_signin_desc">Connect your Spotify account, or pick your own catalog plugin.</string>
<string name="welcome_guide_signin_desc">Connect your account, or pick your own catalog plugin.</string>
<string name="welcome_guide_search_title">Search and play</string>
<string name="welcome_guide_search_desc">Find any song, album, or artist, then press play.</string>
<string name="welcome_guide_offline_title">Listen offline</string>
@ -553,6 +553,7 @@
<string name="jam_error_broker_host_empty">Broker host is empty</string>
<string name="jam_error_connection_failed">Connection failed</string>
<string name="jam_error_connection_lost">Connection lost</string>
<string name="jam_error_broker_unreachable">Couldn't reach %1$s:%2$d. Check your network or pick another broker in Settings → Jam.</string>
<!-- Settings: Group Jam broker -->
<string name="settings_jam_broker_content_description">Broker</string>

View File

@ -33,12 +33,14 @@ import org.spotube.spotube.modules.settings.JamBroker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -47,6 +49,7 @@ import kotlinx.serialization.json.Json
import org.jetbrains.compose.resources.getString
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.jam_error_broker_host_empty
import spotube.composeapp.generated.resources.jam_error_broker_unreachable
import spotube.composeapp.generated.resources.jam_error_connection_failed
import spotube.composeapp.generated.resources.jam_error_connection_lost
import spotube.composeapp.generated.resources.jam_error_no_broker
@ -135,8 +138,15 @@ class JamRoomClient : KoinComponent {
val mqtt = startClient(broker, clientId)
client = mqtt
withTimeout(broker.connectionTimeoutSeconds.seconds) {
mqtt.awaitConnectivity()
try {
withTimeout(broker.connectionTimeoutSeconds.seconds) {
// connectionState replays its current value, unlike awaitConnectivity's
// broadcast flow, which loses a CONNACK that lands before it is collected.
mqtt.connectionState.first { it is ConnectionState.Connected }
}
} catch (e: TimeoutCancellationException) {
throw IllegalStateException(
getString(Res.string.jam_error_broker_unreachable, broker.host, broker.port), e)
}
_connectionError.value = null
log.i { "Connected to ${broker.host}:${broker.port} room=$code as $clientId" }

View File

@ -149,8 +149,11 @@ class PluginManager(
val state: StateFlow<PluginManagerStates> = database.settingsDataStore.data
.map { p ->
val json = p[DatabaseKeys.PLUGINS_STATE_KEY]
val defaultSelectedPlugins = mapOf(
PluginAbility.AUDIO to NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN)
// Each category starts out with the built-in plugin that provides it.
val defaultSelectedPlugins = BUILT_IN_PLUGINS
.flatMap { plugin -> plugin.abilities.map { it to plugin } }
.distinctBy { it.first }
.toMap()
if (json == null) {
PluginManagerStates.Data(
plugins = BUILT_IN_PLUGINS,

View File

@ -18,15 +18,6 @@
package org.spotube.spotube.modules.search
import org.spotube.millennium.composables.MillenniumMarqueeProgress
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumAutocomplete
import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.theme.MillenniumTheme
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@ -60,9 +51,6 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.rememberTextFieldState
import org.spotube.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -77,17 +65,32 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.composeunstyled.Text
import com.composeunstyled.currentWindowWidthBreakpoint
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.spotube.millennium.adaptive.WindowBreakpoints
import org.spotube.millennium.composables.MillenniumAutocomplete
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumMarqueeProgress
import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
import org.spotube.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import org.spotube.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
import org.spotube.plugin_interfaces.plugin_apis.metadata.search.MetadataSupportedSearchType
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import org.spotube.plugin_interfaces.plugin_apis.metadata.user.MetadataUser
import org.spotube.spotube.Platform
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
import org.spotube.spotube.core.audioplayer.QueueEntry
import org.spotube.spotube.core.jam.JamRole
@ -108,6 +111,7 @@ import org.spotube.spotube.core.ui.component.UserCard
import org.spotube.spotube.core.ui.component.cards.PlayableCard
import org.spotube.spotube.core.ui.component.dragScrollable
import org.spotube.spotube.core.ui.misc.SkeletonTree
import org.spotube.spotube.core.ui.theming.AppTheme
import org.spotube.spotube.getPlatform
import org.spotube.spotube.isDesktop
import org.spotube.spotube.modules.blacklist.BlacklistRepository
@ -119,46 +123,38 @@ import org.spotube.spotube.resources.iconsax.Iconsax
import org.spotube.spotube.resources.iconsax.IconsaxSearchBroken
import org.spotube.spotube.resources.iconsax.IconsaxTrash
import org.spotube.spotube.resources.iconsax.InconsaxClock
import org.jetbrains.compose.resources.stringResource
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.search_clear_history
import spotube.composeapp.generated.resources.search_placeholder
import spotube.composeapp.generated.resources.library_tab_albums
import spotube.composeapp.generated.resources.library_tab_artists
import spotube.composeapp.generated.resources.library_tab_playlists
import spotube.composeapp.generated.resources.search_results_title
import spotube.composeapp.generated.resources.search_no_search_types
import spotube.composeapp.generated.resources.search_start_typing
import spotube.composeapp.generated.resources.search_clear_history
import spotube.composeapp.generated.resources.search_loading_albums
import spotube.composeapp.generated.resources.search_loading_artists
import spotube.composeapp.generated.resources.search_loading_playlists
import spotube.composeapp.generated.resources.search_loading_tracks
import spotube.composeapp.generated.resources.search_loading_users
import spotube.composeapp.generated.resources.search_no_albums
import spotube.composeapp.generated.resources.search_no_artists
import spotube.composeapp.generated.resources.search_no_playlists
import spotube.composeapp.generated.resources.search_no_results
import spotube.composeapp.generated.resources.search_no_search_types
import spotube.composeapp.generated.resources.search_no_users
import spotube.composeapp.generated.resources.search_placeholder
import spotube.composeapp.generated.resources.search_results_title
import spotube.composeapp.generated.resources.search_searching
import spotube.composeapp.generated.resources.search_section_tracks
import spotube.composeapp.generated.resources.search_section_users
import spotube.composeapp.generated.resources.search_start_typing_tracks
import spotube.composeapp.generated.resources.search_loading_tracks
import spotube.composeapp.generated.resources.search_no_albums
import spotube.composeapp.generated.resources.search_loading_albums
import spotube.composeapp.generated.resources.search_no_artists
import spotube.composeapp.generated.resources.search_loading_artists
import spotube.composeapp.generated.resources.search_no_playlists
import spotube.composeapp.generated.resources.search_loading_playlists
import spotube.composeapp.generated.resources.search_no_users
import spotube.composeapp.generated.resources.search_loading_users
import spotube.composeapp.generated.resources.search_see_all
import spotube.composeapp.generated.resources.search_searching
import spotube.composeapp.generated.resources.search_warm_crates
import spotube.composeapp.generated.resources.search_start_typing
import spotube.composeapp.generated.resources.search_start_typing_tracks
import spotube.composeapp.generated.resources.search_tab_all
import spotube.composeapp.generated.resources.search_warm_antennas
import spotube.composeapp.generated.resources.search_warm_cosmos
import spotube.composeapp.generated.resources.search_warm_crates
import spotube.composeapp.generated.resources.search_warm_music_gods
import spotube.composeapp.generated.resources.search_warm_records
import spotube.composeapp.generated.resources.search_warm_speakers
import spotube.composeapp.generated.resources.search_warm_vinyl
import spotube.composeapp.generated.resources.search_warm_music_gods
import spotube.composeapp.generated.resources.search_warm_antennas
import spotube.composeapp.generated.resources.search_warm_records
import spotube.composeapp.generated.resources.search_tab_all
import org.jetbrains.compose.resources.StringResource
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.spotube.millennium.adaptive.WindowBreakpoints
private val GridMinCellSize = 180.dp
@ -183,7 +179,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
var currentUserId by remember { mutableStateOf<String?>(null) }
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
val blacklistedTracks by blacklistRepository.blacklistedTracks.collectAsStateWithLifecycle(
emptyList()
@ -207,10 +203,6 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
}
}
LaunchedEffect(Unit) {
currentUserId = libraryRepository.currentUser()?.id
}
val focusManager = LocalFocusManager.current
fun playSingleTrack(track: MetadataTrack) {

View File

@ -30,6 +30,9 @@ import org.spotube.plugin_interfaces.plugin_apis.metadata.search.MetadataSupport
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import org.spotube.plugin_interfaces.plugin_apis.metadata.user.MetadataUser
import org.spotube.spotube.core.db.Database
import org.spotube.spotube.core.di.injectLogger
import org.koin.core.component.KoinComponent
import org.spotube.spotube.modules.library.LibraryRepository
import org.spotube.spotube.modules.saved_tracks.SavedTracksRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@ -81,7 +84,8 @@ data class SearchScreenState(
class SearchScreenViewModel(
private val repository: SearchRepository,
private val savedTracksRepository: SavedTracksRepository,
private val database: Database) : ViewModel() {
private val libraryRepository: LibraryRepository,
private val database: Database) : ViewModel(), KoinComponent {
companion object {
private val RECENT_SEARCHES_KEY = stringPreferencesKey("recent_searches")
private val SEARCH_TYPES_ORDER = listOf(
@ -97,11 +101,16 @@ class SearchScreenViewModel(
private const val ALL_ROW_PAGE_SIZE = 12
}
private val logger by injectLogger<SearchScreenViewModel>()
private val json = Json { ignoreUnknownKeys = true }
private val _state = MutableStateFlow(SearchScreenState())
val state: StateFlow<SearchScreenState> = _state.asStateFlow()
private val _currentUserId = MutableStateFlow<String?>(null)
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
private var queryJob: Job? = null
init {
@ -113,6 +122,15 @@ class SearchScreenViewModel(
}
loadSupportedSearchTypes()
viewModelScope.launch { loadCurrentUser() }
}
private suspend fun loadCurrentUser() {
runCatching {
_currentUserId.value = libraryRepository.currentUser()?.id
}.onFailure { e ->
logger.e(e) { "Failed to load current user" }
}
}
fun onQueryChange(query: String) {

View File

@ -98,9 +98,9 @@ data class UserSettings(
@Serializable
data class JamBroker(
val name: String = "",
val host: String = "test.mosquitto.org",
val port: Int = 1883,
val useTls: Boolean = false,
val host: String = "broker.hivemq.com",
val port: Int = 8883,
val useTls: Boolean = true,
val username: String? = null,
val password: String? = null,
val clientIdPrefix: String = "spotube",

View File

@ -30,7 +30,7 @@ import platform.WebKit.WKWebViewConfiguration
* Mobile Safari's exact User-Agent on this device.
*
* WKWebView's default UA ends in `Mobile/15E148` without Safari's `Version/x … Safari/604.1`,
* which sites like accounts.spotify.com treat as an embedded browser ("Oops, something went
* which sites like accounts.greedygreencorp.com treat as an embedded browser ("Oops, something went
* wrong"). Hand-building the string isn't safe either: Safari freezes the OS part (iOS 26
* reports `iPhone OS 18_7`). So WebKit builds it: a probe WKWebView gets Safari's application
* name and reports `navigator.userAgent`, which is cached for every web view.

View File

@ -28,6 +28,6 @@ data class ScrobbleTrack(
val trackName: String,
val artistName: String,
val albumName: String?,
val streamingProvider: String, // e.g. "spotify", "apple_music" etc
val streamingProvider: String, // e.g. "greedy_green_corp", "apple_music" etc
val durationMs: Long? = null,
)