From 61d34963fa0d6da3548758570a7c1f585936d2d2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 15 Aug 2025 21:39:05 +0600 Subject: [PATCH 01/47] website: ads not showing up --- website/src/components/ads/Ads.astro | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/components/ads/Ads.astro b/website/src/components/ads/Ads.astro index b2e4bb27..9d3daf1c 100644 --- a/website/src/components/ads/Ads.astro +++ b/website/src/components/ads/Ads.astro @@ -12,7 +12,7 @@ const { adSlot, adFormat, fullWidthResponsive = true, - style, + style = "display:block", adLayout, adLayoutKey, } = Astro.props; @@ -22,7 +22,7 @@ const AD_CLIENT = "ca-pub-6419300932495863"; Date: Fri, 12 Sep 2025 00:19:00 +0600 Subject: [PATCH 02/47] website: add back download buttons --- website/src/collections/app.ts | 163 ++++++++++++------------ website/src/pages/downloads/index.astro | 29 +---- website/src/pages/index.astro | 4 +- 3 files changed, 87 insertions(+), 109 deletions(-) diff --git a/website/src/collections/app.ts b/website/src/collections/app.ts index 3ae86c8a..22e567a9 100644 --- a/website/src/collections/app.ts +++ b/website/src/collections/app.ts @@ -1,105 +1,110 @@ import type { IconType } from "react-icons"; import { - FaAndroid, - FaApple, - FaDebian, - FaFedora, - FaOpensuse, - FaUbuntu, - FaWindows, - FaRedhat, + FaAndroid, + FaApple, + FaDebian, + FaFedora, + FaOpensuse, + FaUbuntu, + FaWindows, + FaRedhat, } from "react-icons/fa6"; import { LuHouse, LuNewspaper, LuDownload, LuBook } from "react-icons/lu"; -export const routes: Record = { - "/": ["Home", LuHouse], - "/blog": ["Blog", LuNewspaper], - "/docs": ["Docs", LuBook], - "/downloads": ["Downloads", LuDownload], - "/about": ["About", null], +export const routes: Record = { + "/": ["Home", LuHouse], + "/blog": ["Blog", LuNewspaper], + "/docs": ["Docs", LuBook], + "/downloads": ["Downloads", LuDownload], + "/about": ["About", null], }; const releasesUrl = - "https://github.com/KRTirtho/Spotube/releases/latest/download"; + "https://github.com/KRTirtho/Spotube/releases/latest/download"; export const downloadLinks: Record = { - "Android Apk": [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid]], - "Windows Executable": [ - `${releasesUrl}/Spotube-windows-x86_64-setup.exe`, - [FaWindows], - ], - "macOS Dmg": [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple]], - "Ubuntu, Debian": [ - `${releasesUrl}/Spotube-linux-x86_64.deb`, - [FaUbuntu, FaDebian], - ], - "Fedora, Redhat, Opensuse": [ - `${releasesUrl}/Spotube-linux-x86_64.rpm`, - [FaFedora, FaRedhat, FaOpensuse], - ], - "iPhone Ipa": [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple]], + "Android Apk": [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid]], + "Windows Executable": [ + `${releasesUrl}/Spotube-windows-x86_64-setup.exe`, + [FaWindows], + ], + "macOS Dmg": [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple]], + "Ubuntu, Debian": [ + `${releasesUrl}/Spotube-linux-x86_64.deb`, + [FaUbuntu, FaDebian], + ], + "Fedora, Redhat, Opensuse": [ + `${releasesUrl}/Spotube-linux-x86_64.rpm`, + [FaFedora, FaRedhat, FaOpensuse], + ], + "iPhone Ipa": [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple]], }; export const extendedDownloadLinks: Record< - string, - [string, IconType[], string] + string, + [string, IconType[], string] > = { - Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid], "apk"], - Windows: [ - `${releasesUrl}/Spotube-windows-x86_64-setup.exe`, - [FaWindows], - "exe", - ], - macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], - "Ubuntu, Debian": [ - `${releasesUrl}/Spotube-linux-x86_64.deb`, - [FaUbuntu, FaDebian], - "deb", - ], - "Fedora, Redhat, Opensuse": [ - `${releasesUrl}/Spotube-linux-x86_64.rpm`, - [FaFedora, FaRedhat, FaOpensuse], - "rpm", - ], - iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], + Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid], "apk"], + Windows: [ + `${releasesUrl}/Spotube-windows-x86_64-setup.exe`, + [FaWindows], + "exe", + ], + macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], + "Ubuntu, Debian (x64)": [ + `${releasesUrl}/Spotube-linux-x86_64.deb`, + [FaUbuntu, FaDebian], + "deb", + ], + "Ubuntu, Debian (arm64)": [ + `${releasesUrl}/Spotube-linux-aarch64.deb`, + [FaUbuntu, FaDebian], + "deb", + ], + // "Fedora, Redhat, Opensuse": [ + // `${releasesUrl}/Spotube-linux-x86_64.rpm`, + // [FaFedora, FaRedhat, FaOpensuse], + // "rpm", + // ], + iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], }; const nightlyReleaseUrl = - "https://github.com/KRTirtho/Spotube/releases/download/nightly"; + "https://github.com/KRTirtho/Spotube/releases/download/nightly"; export const extendedNightlyDownloadLinks: Record< - string, - [string, IconType[], string] + string, + [string, IconType[], string] > = { - Android: [ - `${nightlyReleaseUrl}/Spotube-android-all-arch.apk`, - [FaAndroid], - "apk", - ], - Windows: [ - `${nightlyReleaseUrl}/Spotube-windows-x86_64-setup.exe`, - [FaWindows], - "exe", - ], - macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], - "Ubuntu, Debian": [ - `${nightlyReleaseUrl}/Spotube-linux-x86_64.deb`, - [FaUbuntu, FaDebian], - "deb", - ], - "Fedora, Redhat, Opensuse": [ - `${nightlyReleaseUrl}/Spotube-linux-x86_64.rpm`, - [FaFedora, FaRedhat, FaOpensuse], - "rpm", - ], - iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], + Android: [ + `${nightlyReleaseUrl}/Spotube-android-all-arch.apk`, + [FaAndroid], + "apk", + ], + Windows: [ + `${nightlyReleaseUrl}/Spotube-windows-x86_64-setup.exe`, + [FaWindows], + "exe", + ], + macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], + "Ubuntu, Debian": [ + `${nightlyReleaseUrl}/Spotube-linux-x86_64.deb`, + [FaUbuntu, FaDebian], + "deb", + ], + "Fedora, Redhat, Opensuse": [ + `${nightlyReleaseUrl}/Spotube-linux-x86_64.rpm`, + [FaFedora, FaRedhat, FaOpensuse], + "rpm", + ], + iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], }; export const ADS_SLOTS = Object.freeze({ - rootPageDisplay: 5979549631, - blogPageInFeed: 3386010031, - downloadPageDisplay: 9928443050, + rootPageDisplay: 5979549631, + blogPageInFeed: 3386010031, + downloadPageDisplay: 9928443050, packagePageArticle: 9119323068, // This is being used for rehype-auto-ads in svelte.config.js blogArticlePageArticle: 6788673194, -}); \ No newline at end of file +}); diff --git a/website/src/pages/downloads/index.astro b/website/src/pages/downloads/index.astro index 72479a82..23fe938e 100644 --- a/website/src/pages/downloads/index.astro +++ b/website/src/pages/downloads/index.astro @@ -22,34 +22,7 @@ const otherDownloads: [string, string, IconType][] = [

Spotube is available for every platform

- -

- Versions of Spotube (<=v4.0.2) are ceased to work with Spotify™ API. -
- So users can no longer use/download those versions. -
- Please wait for the next version that will remedy this issue by not using such - APIs. -

-

- Spotube has no affiliation with Spotify™ or any of its subsidiaries. -

-
-
- -
- The new Spotube v5 is still under beta. Please use the Nightly version - until stable release. -
- - -
- +

diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 686e435e..c2acd0fa 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -53,11 +53,11 @@ import { ADS_SLOTS } from "~/collections/app"; From 60fbf66639c9c30a4c9a4557b39b4d401f5aed17 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 12 Sep 2025 00:26:56 +0600 Subject: [PATCH 03/47] website: use locked version of astro-pagefine --- website/package.json | 4 ++-- website/pnpm-lock.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/package.json b/website/package.json index 9f1eb71a..74026b97 100644 --- a/website/package.json +++ b/website/package.json @@ -17,7 +17,7 @@ "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "astro": "^5.12.8", - "astro-pagefind": "^1.8.3", + "astro-pagefind": "1.8.3", "date-fns": "^4.1.0", "markdown-it": "^14.1.0", "react": "^19.1.1", @@ -36,4 +36,4 @@ "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.16.0" } -} \ No newline at end of file +} diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index d297e8c6..173f5e5d 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -33,7 +33,7 @@ importers: specifier: ^5.12.8 version: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) astro-pagefind: - specifier: ^1.8.3 + specifier: 1.8.3 version: 1.8.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)) date-fns: specifier: ^4.1.0 From 0e48b7a33775f0bd92faed4a3aedce9f49ff83de Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 12 Sep 2025 00:38:35 +0600 Subject: [PATCH 04/47] website: update logo --- website/public/android-chrome-192x192.png | Bin 12127 -> 25014 bytes website/public/android-chrome-512x512.png | Bin 64675 -> 145860 bytes website/public/apple-touch-icon.png | Bin 10898 -> 22270 bytes website/public/favicon-16x16.png | Bin 390 -> 691 bytes website/public/favicon-32x32.png | Bin 777 -> 1485 bytes website/public/favicon.ico | Bin 15406 -> 9608 bytes website/public/images/spotube-logo.png | Bin 91271 -> 147637 bytes website/public/images/spotube-logo.svg | 349 ---------------------- 8 files changed, 349 deletions(-) delete mode 100644 website/public/images/spotube-logo.svg diff --git a/website/public/android-chrome-192x192.png b/website/public/android-chrome-192x192.png index 2ef2f3e7ccee385755de9846c7df154518b1b0fb..9f1d8bcc3e71a82807fe7cb8386e185f3d3a5c31 100644 GIT binary patch literal 25014 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%0A|+{$?X6+M63BfEA4U1omLnI>Cy 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 literal 12127 zcmeI2)mI!%)b57?g1ZK{;O-8=2?_255AN>F;O_433@*VfxO;FXxVs+Sn{)n!v)1=r z?B3Pg)lb!WYW1$V_=PFTOQRqXAOZjY6j>Pw<&WI`U%-F*xGJw&!+j(uCuM0dK-C1% z;m3`HiKeXSj~@W~k2X92D#QW+^Iw;bAovIX06G^60R54n{(F`S{J(FzbD{rV`@fDq z1o%b(0Ony?iLa{eP^VdsDMX7d8+PX$LeKs^29!`hbSUL~s)U7jW1TvRXDXVAm_(v~ z4_%ic38aql@|m_Aq}gwf0cmDellSAz`SHps%}LM5TiGh=$~#y zIWkOx(h3n=VN5b3gVHbP2&{ASq)4O)=xnTWn4vKEM@KP1F)*R{_?8F~KxCKO!Cq;g z1Tu0X3yz;oB^1S{U~OO=KwS5=_96~|0WHq-F-~69o92TIIMnZdz;I4#uR@Lm z3eN9}ABMT_$D@MJw8(>8{jhHl&Mv9}?<8;WH?~yb)Foyvj3O1G#_ee^z&}VG{(Zt* zW>N@`@EQRd**I$owQvZC7%yosIeToFn_Ax%cao*x#$d_yQEYkU-~D~bq`|FKaN`>c z#hd3z*&b)XLLWAR05I6oC;cO-Nh3Szj8Qsenw0aHVCfh1g2m&DHk*z@ldQ9Bmo}XR z3pD_2l{TC4EP;i;EuYBME9U~JEM?(U z3b~X7;%I#;E-=)V--u;*wjVV0;{c>`-{G?P*;R;|y}$Z9QgFadrAR>()9R+5EHW}Z z#|LQi>Xc0gAo?f-U5c`Bf#zD(5M0A?SHeXbr=8}e z4v#Ra3Um`17)lPP2MT2}pr3f=<+IzjT>G=$V+)iQ`L1bb%qjC|YnL9Lo~(*T?7CWiJObr;O$4rBzpB z1ChxGHPUY}WX_gjGvIUF;Dpb|vif`yRRDk^LP0gZQy-en1{33QaTO@4Ykd zV%R*I!M^g!wvXG@c3J(p(}p|QhH$oMNo%Lx@Ra-4!Av+{)}R^>5;?5uCo_MQIzN)Y zt*%ppz(1K#K*LK??`aYrZ5O(3hJ-ImpG5W}OTC2KlyZmV?^7R-jm<4cyTXBV$2#U= z-jY^74hDX!mN{KC24}QKGL5uDZ(fQoz(t~E>b(-?v2eZB-kRpBK{S$lL`=Tx4*;tuRVen+V z=+!H#{(Nkk3N>zX#|Qr?{GI?kv2kBjWTdPe?r^*;E*ZEr-jN~wyuOma{^7imt=Z($ zQ|6_SfUercy>~+)ZK5K=wVjVjenH;yt3c4-u$I%fUb@S)9%-Y-(g(xlC(^z=qWaR! zs7}hsU0=@1YonVD5429WNt7w#0Rb!lL|&(Z8^xTaQXOJ^#!90&6^PF&9njnsdC@?) zU~RKvYc;Ne+1l<018hl0MaheIE#fr1#`<^KA5^{EJ11Fzdt6u`4}_p=o>QS5p9ue< zdio6Mxv#8mmOxE4CgUU#`q7y${^8R*8ggOL=hjLi*UX2eALBm5<&BhMyOwow%bI;r zVO*a>K<5kkE084pWxZ$enr~dSlSt0pMoMMgK-8zp)jZ8=6{OT z!t-_i5|h^IK5=bpAUR?^$M_e%rth(FR^4fGoZ+T;_1zJn%Mydv@}M3O1GiYeaWdmzN#U~o3gc8UKpu9eS?6^))t6kbRI*QWyml$`($VpmR>GFrX1id^^Dsh zq-elJ>DchRq{P4mC4?nydrnn~M-`5^wbc+j=AfIMx+ifGpQO@|;|=yb}ue-pHv}Pf|(+ zsWhv*@{o?D+ls;AM6DW0l#WZNhZxStyJc7rwJgfsdFzRq#xCI9hNlXW=~G8!+_M_N z#E;8o)d#gqiY4bmsy7@n@u1pfG*=8;{+tPAI zz{LnR{kc8?@AMk)tuZQ!@u5*7!PX|SaqYRu^FE}>_ufnW#N3eh6&JqTcf6*R7b7_x zKWVt0+v9{#og||K`ez-?N-NyZ3ZsPA`Lf&T*-SNnlU}PBnclC>5oO!&8~k;=`@*7yB;fF zNHdFHDws~xZF9cuTFUYg`KoiC$JiLyVh|Bgy;4#A0y~NxZLF7jdI_0uAmk`sTdu>E zOKm238B@3QU35)(Yv$q+HlV!{zI))-ZvM|b(0W-E zWWuGN4xL(#{BDT2)`O^heTB^S77_!Paorqj%obf2KbCL5_))>)z4Wc%JL9E}s{>yB z*4-)TN>arJoI>3oSGZoSafE&Y5?&Xq67_|g`_x~PzjYC5vzClYXICptp05H;9WTZV zQ^1>=a}&#lnv$6TP#h}jUpFi{)miDe8J9SJl^LwqM+4u}X@Bc#eQJI4hbvkr2J{%^ zQ26lbpH0KVc%enc89J%rjH$-f^RA!YH*8Lq#!f!Z;32>R5HN$C<-YHq+IGh1Yg4+d ze{v}P6EcXRixjI?jY}Yxp;64T{D7q2JoDgR4LLLvgAc4=uv;oZ>m=k9F_=Lf zug80X-OO=NM6xF?z**(T$aZyo3&Kx>rtXw_&vytRAU!=*&h;_^WLOku(F;wLTXxICG_Z89zFrTh zKxSi?*Z-qR&+i^K`iiiufL0psIh@Nky_=Q|-zR8ktM_TFV1Y0TcIXI1yHVJ;{k!aN z);8G2W)0co*RF+`TYqs(=1y4y!d2l*A~^<@SEcZpZig7dESW8axT(G(asd3@V3NfF zJm8yX*U4$!t*J-o-pA&GH91o~Cxf)xee&C9NBVB_uDXZ#39mP@hTyv1?xh9iv)ZMk z9f8{Pc6|qqRo#Wn^^Crj-hfn}j&rwnaWz57oh=qR^ug1L)c=6Xz?`17nQ2zxewmv5qe5 zID2woaJ!E6WCCm-llWALKR4g*RV0cth()R8k5-zcH^h};C4yJO7=M+)8)`c@pn&MR zx^p-N6||-lXf*Y1Zw`0)70S&)N9*BEW)MLaf-l;Q%;MR);9049y|$ZX#?R({l$&uk z)`**vT^CE6(v(6+7x_OjDrYTzz7r1IHhKkYqTb!a4JW<@Ss%%hlxVU!2;%e?{FPNO zFoDwDSx|tq+)yJOszb3Q*zCrc$Z-4aMlxl3ibvsl7{$lD-JBkS%-R|db#I}tk+QaW z|Fo;&3e!Z1uIAs{x8zZR`)Y^c_xo8i6)5@2WT)sFt)2GjxE5^v@TdHj88rq^=lbmo zSYoD^erfr2&s#iMAKf*+M+$W!YhM9h4go1pjdM#4131Ati&b@k0jPX38XES{&wsC)H644#I7W&1_o}$8OavS*k8kW)FU`j>_u&^vUXqh-=a&gk z_hu`urhdGJrY*2|FkDz9)8~B8roNfN$Q_(AG&_pK85n>>AmpMN!U?qYGoB9gje@n8 zb7(+=|7>1v2+hAqc>zGu#~wAe8EC%7{UPV0Sv5M0^Sf;{$XoSm z+5}REz@+jQNKgL7rxd4f#;CXTy2b4)Yo_I?NK||jk})WtH03Zml%7IZKOggs_we@J zA>aqHhT0XSNy?QQs|uTP6hpb97xz3@CK(pvUf{814&p5>nk8x(zO|y~_G5-nqop;F zwErNpSTZ;UEvseRirgsvuuA*0~5J(YfWR;LdSH^9CmW!^I13j8HU@ zz%jMITQrnLNuNl2!e4pv+WpWKu!N zXJ%rW>Z`x?w?+4Cd+P0bDhyLhvvk>lMp?B5u_Bn8e5KQ7DM8y0nO}@6f2)?@3Ys6- znFYXF5i;ta=R(QU3R@LO%iFUwptFMzPx@v%t_hz8XR3f*B>dAGC8yvjp^G9W zJ2=D8#IU`TIDp1IjaCA($M=rAOAhb3qV=SWsTo6(?-a~&Zl=64rt@Z~kFAN!kG_eJ z%EPG2VRvc~4JsesVHTqosC7nBI-e?z-eUc`ERk)T8eIAf`jRSpv{TB`En(pQnaU9@ zfe;9E?>k37G&-l|yiG^3F~%NkTFh@AxDuMne*!APh}`z8Xg>_U){M+9p0ntiUWFxt zQ`x(PL^$F35FU*rtUlY`=bkI_L zhza%R`e;XJF@)|w_ys6@QzriIj!TPRAudOX)5!xgIVgSsM#Kcu6mk?9OKAmBWKJ62 z)nwZ`YxmdSjfrhfTXHdD#WmgLi97LZxlIJl@SP=z1TDX)4 zC3=6xTjUbsYHH{YG)%ST*3fQgsrJ)gl`N3p2%s>Cfus2!VpAh^2(e0#q5KbnIa9e7 zq_pV^K0(i+nOhkEKBFWMK9DFh4d?+B0IoV#TWW`7U-w0u`>EBDQJ&u3x(oQ(jBP{% zlyaf4gc&3Lt^iau=b}jtkui=88h(+gqE=rVFZsz(6MInl8B|Bae^Xh^+&6??8=&_| ziOx1HZcJzyG^lO{jC|hv=P*uvoEGjm0LH)BFfp+RnWjiy@QA#^TdikSNgpe*$9D$~ zMOWu;hXc9fNHI-8jby3`*;Ug;uZIVw5D5$G-f>DH9i2^#p+GpP@U{$?*fLR`_yPc} z%mb!Tv}dN|Lc$orD`H2qnM%f6w_upXIEGMqY-prsrpQHp-9ate0c;icGq1c78gx+C)%{f9yLspLn4=ZoeTo z2AS`3ok$lQSC5;^x<@Kb8Q42zhOAhSbAF~aFptiiDo)yL*2eO;*LCG2oITpQy%Kb| z#HU2-NO9kdE6THwyR-&xdrW=O;o=@OVZde@J#$T_gO0JL2Z1xBp@*)SB@DLo5be|W z=3DQOw4S*?h;LSpIs#V)O`Qqp4+=<2# z$*W8DaFKD?=jmlFLprX>Pn8|A_T3N2neaMQ79mH~OMj`*E?EsrqHT?zgKa9`m?GAg zgH*ZXAZdC-r4#KV`P^ax(-slf12T`2NV#SNzbFi8!){=2qfBG8E*n8(p;MXjRWHls zf<&A{wqyvo#ce~;6WRaOTS5b}vNq8{E-sWPqNWIfW!*GAKPUFb>*DAvmz;)^TkszxvmbKWe4_XPQw@KOY zN=Z;0EiFTiAm08`?b!-9`!)L{YmK->Z+#D!Be!R~i%-4l`(2lr*84ob^Xl{!d#J2r zShYz#&C%#zN_vAcWh}U4sR5-B%=#(Kff>Pb{-ynK%|1u37R8PO$ zZ9|a9S#Q}FB3=#Pm}f)vuxIu{n#Ruz3lDLGi&0x#Q?7Q43LfkG2`+FUF1bq;<}U#` zCm^PU_c#W6iMQI{W{~3(7l3CN6%}pC?-8!4_NB`m;G1G4Jv~t^7R(2ixudVomnjyK z7(wEeg?FyzF7P8mwq^eLJ;_GnEynF=a6lohT4T6gn{`ji2lw#RV8nyj9177Awmm4< z*!#XJ>3kJt^9J@?L*TktE`Iq#8h-Nv5}wVyUzh&NMh;EEL-Z6I6MsmL{@vlC5h;Gbq*Wgo822E)2i+5YNf<%`(lEJy#s4 z$(ZX{^r|(9>vv36VMv9bPTp2ejP!R`lxO#ukti2{GZ2$sKR zy$;9GM@izT7?rPd99K%-@48kU*H*5!e|wDqP4~oy(4kOAoKHCoBm}B3irz0sT;R|U z3-{PUq`2krFI68$GMU59UeRfd@nQKoTP9!v*9lWsn>4)N z`b=Ep{I+K8@VxsbRzu7pU!lJH?Jrdq1Hrr!Tf2#jjz4vR50B<2-}hu34d(QYw`V~H z0D|{5B>r?_LVFK8p96tcvq+XfBdawl!K_kaF_xxDAVIHgkD<9r z`x=sSjb9*TMayqKWgeUJYa5mbcEc3?nr1#*9$m2Fnh|B$1Y7p}K3*?5dwxC7GqqD? zdAl=M7TU1TgfvV*Zbvry7fZXUxW^Ysp%Wv5EyDyxWd7ROW<%^@lkLLI{XY>0cO=3t z*n?{h$OQsVMeG>Ki`*_%oi;^oAcQHlklm0kltFPyR@s6kKeV%_FiqkDOP^l7HsQ$x z-HyyhEYy80OW-VZyP1yo*3s7Q<}>}<1;~4qYC-)h&n%KutbDo7{VT8+151bI{y_2g(weJ|BF-K%q61F zZ|I`PQ*AB5dheNuC0wkm>ACI?&$${RI*PnLfl(NsGu`_YyV{K*s+RcuyQWtno(9LZ zl7ZsSVzHQh{YtM-yyf;si^qasgLnU`;rhbfmViN7d&R?}pKc9q=%f%Tl4bXSGjmJ!cyWZo4hWK~3^hDC|*v)j=?q(-^&+V8Pc>a{NkQUuq>IWiLtzb`iK%<&s`U-yP_GnL~;4M$Tt1(Gq4x%6}&@gSOG*nKLT&`2{Kv&x9 zbpt;@SthY$Ci;y`cJc0F?lgZF|Cg8m3!*?Hr3*}?Xf4J6G=M?|Mbxk7Q7$^~q#n2B z{`*Yz85l3jpRxdr4Fvzh6&C%>mp7Zb>DznGiiU{SOTWo4-)_+m5J`-W4AWO{wbl z>`$T^gX`7LYN1dbSo|9ldPA)*33PnKGYSXL2hHFK$s`?~`sYIjed<*dUuX7ITL3~X zA_@-Cz$vV47q+EhQl!&34d^TEHq-@2Cn>!kfF}@-uGcOofw)SRtCq?-v7(}`zl9mp z`tKs4DG91jU*Wt#OU+!#UFS!dmi5fyb6>lU!+0ew8M0P5Or?>G3YA);1bd2eRJ#Sy zhOU(To`QrT>RJpOs_%=N`PQAl#Omb&g+`dz&E)940jB6o5%5(lpGeI@Z<5)iTFWrO z5(c}}iuhjW;S>Om8&+IE{^CmWTKxv4{r8nvP^?P6hQ9#8fYFJBH91HwK&1cc@f%Jx z5sT0lks~li**^`j7vUATbaub;BUYzLuJ#vN%6LC`G7bf*6cwnKgkie2Dz7WBKE|@~ z?D^Q?^5$7(qv=Q zWiVpnP*m0#`NI@j@=tdOV7VdUSLAVMfGI z_xJ*zWu^-C=78|_-^7mtu~p@@wVeQ>aa=s9sAPk((tpT#dNx#i80-@J7VoA86_E-I zmHI2{EF1#)2YY^vIu;t0jm1N65l`_K^KJfSLDS6_e3R8dzE63Yhmeus#rq;hsyM)n zdf1fYuG=A~{<%rlsF^@($XM+79&nRw<$KQ(KZHu*ju?DV(>_Mbl$0!+2+Vx0o4@7z zUb(udF#)T$2Zw3yVwP$yw}LlwD-?ZzSE{fL!SD2e`R;M=cu;y3L8^kMy`^)q6k|cnh>hJk3X6<)MrXcv@I5UAC6G-;e1=(oLswh(a0gZZ1a$iET%{s#^()VCjkq%1cIP+BeH3EffEHt-fFr%) zNy8A0G*MmZc1~}4GCtuy=oThW3CZMR=oR|iCPi^i{6zDELK^09kP$Hg?PX$h`zMsi z3fy6rQP0gi`^uhJKJ#%g!R(|l<4^V@%xN&`bj@YRFy3)eQ6U>5MDGTfA{2E!wDIDp z1mZ|u{Yvyao@Fx92_OWO7*vGvi5q;hL(zu5rV zONDNe!w_C*1fsdEPZo6gg<1QuVGKN1=Mr~btvPS%kGrUT8$zS{`@1Iku3CbvrIX~K zw0B`)U-rhERRl7~1*5*IYc~>3DSzT@SvQ#5`83($H!{90Gr9sjc}dbc_5~N>9~_!) za#8p|iIy(YYzY-1q8aaSyK(3tx|KW=Wdd^IF7lcLw8;cF>t40@1sAj`!7C-Iw~q=R zEdC{+@_>Z;!8!Fj1+9y@OI&jN09Xkl%O4d|I(;Bw^o9#Mie1H;J@SG&2~<{r08kO_zH0 z${2oUS0>`Dr;Y`Y(IaSYH$2XHEpb#3dBQ>T8IX6S=10JUfy!y{s5$s!gHB%bLcW&% zCo}jDR6QE^XFOggB_5*4R`BSAEC*PBa4=6UJ7kl9R5&@NPB(GIehoLQ*jgs4j{~zhc&tX@2?^_M4;LT!dJ6@?WCuRL`IDT;wjX zO0C;6Safrj$+ClH;_mDX&d4Dw#h2+c`e*d@D1lgJ{ z$Jh5^2$|k)%L1mCi6vGE07&ncDg9LH{r)P@+m=k3KXoX$s};Aix(YbSOTe!}pnt=# ztaFOn#&QbBBNNE2=bRaO)%Kj6-EaLTzPEUL!?WnC5sldXd2qlzPAWZGM$e!tWrRdP z^7peYTjBcCE-M&D)C>aALBXtf{K9MSQ?Dp9hdm=>Wb<-*e#c`VdsV%x29rBkR_{-s9cGB)9&)R2|&b`O0m z|5L3>v{pasDbCl~UO-25c29Bd$<<~^DC+yu0ml8_7ePRuixw(O{1TZs6=QgEOo?0i zcNJt?#KS+cXrheQSHg|vh7L4krr>+W!(bVLk-Xe>8X4nJBb3FgjKvBj3HR`fkl&=BeWXdO^lv z#8!y{4+zAly7QJ>!n*mLY$|B1P3M=&(X(En4TQq&T769)*lvWh=Fbb+jQfh#p})v> z)&KqlHp^8lQH`B{M*IW7VE?MaLH>OiG0(fg`zmCI7I+x;qic6_0c}t1TyXPjE;Y&b z9p|{?g+h)Go-(cRvcB1>Af-wN$An_TWQ0e?b~E^^+HLts+@48=UKbX(OL?2+0QPoS zIbO83hv^JG&$-Uldia+%2}Wbo-wmF8id1JFqe8EI<6 z=^k9zfjz|>EE^(=^5FGOAHyOoT$4lBq12Xzh_xQgLCfmdE`G6b>AtNy!M-95&sl(H z3A>Z!=eO$rbdJzn<}Ep5AZ^OXS3h)j+3wWs6oDu7w zr`nGaTrOi5P&oyc`f~ckN~I;sShYeAFsKgl?DH}+8bhq^{PQo8!5qZM4w0~7f&cUq z+V{7fWM%IiYPF`drp2!r0+y`0r>k-sKz9e82NGplX;OMydrd1;7^i1`=oKH2pTVy% zzVMmgJC41)j4}kPY!MqB4Lj{)Y`cz}^sPN2XH`Oh3sJOVqThYH$1^QVK08sOn03^3 z2N(*aeQ4TjR}nX4Y%u1hS{^R_0~@W@YTW4Zs|_l%=sIBNL*WoJ|DV%JDmBwyquiGj zZr1lv{-;beZtw7fi(){Os9+DMGz=K~w#{7DCpvo>gr^Ln-F^|+3W%M~>090DyfhPE zrvcjgFe#8?oBT}KX;R8=W5=g=#nu*{lbjk=;ZSVB{V9??$yTH$ZkEQz&UT^6_gu~D zr=VP`Cz{y&$Z>^w+o)+r`^>Nlr^mgaHXP3sbvvMnXV6T(Ma2ovahQim11e$qKs+hb z?&f@>x%Su`gaFiEM;!h+LV*u9ws3uW&_@;JL&fn6zez7LD*K(@bgQ@0aQ7>V=?#C& z9-tWXlp?a!S2aIld`Ea)^yfIi+H6#+YR$-P_lIay@UF?{qDChESrPrFgob!M2INgjsuwx(uh4D-U+U zHDy+AkY1wDhmE58E}#A1I5*l<<1fkvq|+j*!byZjVveHKEm$^oQRDi$dUeA&T_&^V zdOQ6qeu^W`w3Aq7?=7gTVCt!YtEzIoE~iO)k-xR4lRg8k3Uww=R4l!OCVF6|ZV^GA z+~~rgytN#!AHna*i#CDe-3BDNVJ6Ymm%=qUFTEo)(OPbX$;rSvMxlmb9Xal0qX;pX z;^Z&R--KGFIc{OP>cVJ*LKMjr>Uqwzla$i-!zAlvyVuN@;hDs3OmM)6`$v6KEK93( zY)`y8#tA1*5sx7r#bKf}LWgEd5nXf%?nvXw^G)V!XF?56D=F45J@E|aXQG%0Cak(! zt3iu!&CYseGdev4cGC#flVjQ$O1XEdFv}STq*X;o%xldJC1XLrtWHcYev88-^_j3e zs|*~XQMgi;VN~xy5P=_OXxs+DYuxNt(#P?CVvKn(g(!}<`GSoao*6;`BD<1g^QB2M z*h7!|+QzQSO%q+4KA)0QT2fSW$bbhLXyf|JHvEQ`Xc{)V|x0?lbh4o2}$z z;N19Lb*^%di}Q4-=TabA5K*~NNGRu*eHB^iuNg6b9sB7u#!gOk@WR&}vt{8`l+Brz z%)!s{IS)DoGJdLX7dW4>ON4>-6Pdp=pU@#Cm@@go3Mm)`+0;n8tJ!EJPv>2mizeiyv^`{DJ&E zwY?dgIejC;Br5?JU%Y)_#G;@@{V^Z;4={Ka+W>ie<3r%!{3)VklI}w@6Bya^tn(r8 zstaPf=ljr%RROcPgFb-c+^%Dmec_T1O-Dr5Pv>?Y0yl22{j2}}cT)fVJGJ=^B1p4=DAFJ$IZ7Hv z=NRAldOtq@!*{>jyC3I1=f2K$u6$nC{le5(mx`Q?8~^}N>Fa5k0{|exUl0I7Lbx1- zj$IHgWPWTj+24@QiszOE9tJb`qny87!O>sNOXnK}-uJ4w~EDMeR24ZtgahSCxAaX@d{& zLyD)-vLTWfd9qEVj@iRIYgzg-Sr-E#^MQjA^ZfC0UK16zU)=|?|Nr|VoXH5`6(Z-= zq2P5Ve5m@Nw??8GIWPW_^U7pXHTUw>gx40lFl{+Xdw9lGjM| zO@Kas&rPer5#%3fB9Fx;o@8Rm*8E{A)ly@JI4u?Ijz*$xJhU$!>XiUJ6xXZyk8$H8 z%@n*p$$8z&c}#%#YK_F}k_COl5t6na@G$#6PvH_1^zP0W%X`zMYr&1Y3qYZ0a@k zuBle4ii-^yUz4+ej0jJWzWq;Hly5#krY~CXoLfTn>&ef0r~@X%u+PWWPD+803)cJw zDgExO{~k1`{ZI(Px7neu4UczIIlC&Z_u%k4s(JIBSlb@ev;X)oo$M8r_Gr*G$sb=z zbv8bTvgXgYm2_59Mop=Af_VI&fJTRYO1-?=jK6-dvuHnwtD(DDu)H+n*|~7>kh|tT zIo@euKR)D^j*YI3dwpDnee-)MKP_M}_aXPM|As3cDC-knp>F8gu^&~&aLad~sd`k= zmH!mLuAihFdHs#*V&eLFG>nKSa6Gi+KjJS_yc78jR0U2UhWf~N4l{#=zX>(t#0)c4SZ2aHBVLG0bqJ87q zkLd>I>mjtwCU#k4v~sF%%SF!XhWBvgA0Fm2mUJ{3n(6)n(DJC=({3OA4=lTf|JR zYn`yOpH<5x6Y;_eB1;*I)jAfK@peVr z-fHl}5&2#M(@0)|xm?5O#7uw|yvr z5t=A)Dd0*NEprpmNPHd-y{@q(%u}* zuqK+r*c*`%50w{L@FmRleDpYJ?z&pk&-{WcG>9gC{OUO0Z`YD2GEKqeAGg8svRfE? zVq=Yo>$TlWZsIlW167Goy>}LJ3))3Ondx1DssA_tE-%%KzP+V(f7Zi6o~U^S+kYK$ z*%HRxeRzlNDw%3k7^yrCa5Vm7O+t{hj)DFQ=Q5CRj^{Q;n7#VO+2xz%V4uA8uxRGJ zg`gbl0oOg#eSQTa@@+beenyQkKgF2;NU2MFw_k4?leX7OVr{05J`t-n^s6>pxc<1? zy@MAGSDT%uuf8-qSvyTVjnhaR4|W*NuBRV;I;Lt4@lO`r7rg8`pg+(;loSNC%y^5j zCw@S3567ZVpwng=R3G*zf8e_G#@V$gKyW1D%u$qf0u>iiWC_y^&$am{?!*J?7Q zT+fO6$(+Av%K8{Rxjnsex{B@RVUq@+_u+uU5X4=aGoQCGrY|N&nUj3v-G4DwupFGjmnIL)w za6N`p3`qWl^j0V^r59(So%S3%D!NzJEO425-lI?t&STbakcuza37G#vFgkSo=XAd* zu@4N6E>(RN*RZ)cFyTQs03v6R|Mlk&|VOJ8gdL@Qdi_bxfK3H!zJ6cS! zhKqWq{{_Kk$$949y9ad{usf(ESbg;$^F}9ROW-4|sfTX8CyZ&|#4N&T!e@odhJz2^ z1{|*SHnOwqu@5b`fG>M-zHT#IX{BreAkt33b$h5^EY zjWRFcbDBzRvK7Tw6@^#3kU ze7Tb29$-)HDF`@Jn*J%wsg|IN*S~V5SdSOeA-waYAmKRrVX>SIVGuA)BBkYcZ7w!K zo3+ipMxLm3BjxU_kR=&l(OTk6b)2L+NwdIH-%Wv@(TcR+6&4DBYk3oGWuv6x?>!6y zPE=fBjj*THUj$>MOvW-!n;i>a*z$)+wRx^6x7b1(>EuT~`0c+=*X7rP6qZ^JXgG7G zUj&sC%D_@ifs0GMAn7HudJUhS3lJuFg;e4mml?Dm*sV)M<(KIN4_(~Sti%FEGX2%Q z`-M*j2Ip8n*I;AqZ04SKqo$Z;_vA9M4>RXO@84wPreapqQBIr!5zoY0An9C?G7=d{ zXY5WuOf3BF#Uwd+tfWh^|DT0p=*RMHr-nlisSk3g1x`|ZJKjE`18mVf8a$#7(~bn9{3ECCqWu*R z@RoR}W?hlSH#V1n>5yAjeSz)JrE04M11IgPP}!>_06c511p)5`Uupe~lr3p?JMrmI zG72j|W#xG~gwOq?VrW!(6P=R;k0&#yLx8?&EF8Co*V~AKynGyDlisj5EQ?QdN2Hyg zZaBdlGY&HeCP(!CV8*j{ErMIQt^hf2i!QzBt?#A)wBEf;b8BizYo4ohXgFxGe;Y*6 zFZX_Hl0MAnyVym`%nqH(H@v(&AjQ2^@(L?LhSuQgEU;BC-F}ETZ_(nQX>k3q(|Gq} zKSo1gA2dPEyUDU9OR%I?E=r>@ujw#)C(KGOc6H|IB!&O>LN7%54y|rotfr5&q-=ER zppzTHU0OaQPgMLS%i<@kcaP-{gD0KV>xC8k%f91GU#x~X?7Y=IauD@x?k{ha;r zUg$JY4?fybF$4y;qONnV1O>Kp7>$b#da~gL(2}q6#V7!g-6_W{#MjGi>&2l*KSmvT z*WF>ah@vF;8*@7BPQ$Y&CtLGgPEJa)H_o6@={aj;18#Y9F;iRSbHPV+PqQSg1>qkz<9NC|Z z-ti#k@a^A|EE>1cGa}nw;A|Bpb6{sreJid8&rwHzT+e6{^cJA|v6EZ!MNLPEV6LMhELj6@@sTcIZ5Aj(5vGD}0b)Cjr_QXC~8uC%VHmVgXxx2TtUAP*0@NcXXuT4K-`wx)> z?)~g%jZt3h-VFxHeXU69J$IUFX9w)j&FkOFD<+MW4{ozp#8m2Hc-n6bAuRgl7605G z2i1~qbD-@tz;^0q_L^riFtDB1q>(QLt$@vB&(%?&@&+RK06T(Gye{6AMb2W#`G(D# zi3B1gT+zJbEd8RIsrTGECVv(EaQQ|(3s5fzRxjAs_{z6Xq+)7&fXvD1F3~fkt*OeL zNFSot7z%tJ}fj3TMK-nX@|QbZK5~ELXf4{X?&R*_eHE9u?fPvHV?4 zvluAySGRBTb-kfo-@0PCuSlFE>3zocSH{p{a|Kno&35 zzth+cGrDbY%~;9}#L2;z4Mhc{Iw>VJ%wLqs;l(4aa;CF7PlVIWh12erCNA$N+l0tl z?TDS=$Xbcmq%yz@9Wn{%-kzqX2AZ72w*2;I3S5PiGa*K%OgMyq!(Z z6iKLvt-vIrbd0qC^mhqJ z%}Hv5Cl^jk^KC#5#^ly_Y|-#qQH!&gGsfF;sQ+Z^2WgbE=Ihtkhi680+uxcdzWQ>C zW$7B6ay~ra2#AfUB0mx}C|(!@wi{AxiY87LEX*+%SgR#YU-5dV*CstfI*E<3_r<9E(pl50gBVVVXGg~{v)?|H)kM z_ z0H*p4Jm_hY!Ttr`pewPW*^H7&-;+~KMleomIU;=6#B$!3fggMRN;Udc-(=r8t6i`O ztCU;2;{(A+;XlQStWmycJv8wp%6e|w{0pCWW?VS5K3VYm7Di`8vqc&SbxuB|izI9f zHN|k2V-kn@p}5zONgdO7xab?ii>s7;hB_A9YiOrL+|J-1-X#Z5I}*pVcrEy<(IZ}Y zE&8fa0RQ4oCi#YU+1ibeJ863jO(6Vj*#+Og($@TR=H7oCiM|~<@VRsHNSv4jawC_- z`2tJ~{dpHY>{4iLW@;60M-Z=*w1sDIbp{58ySqRevRXl&!~pW?ABoSR$7S}mW`VkE z$dsR)(uzbrF34IApoPngkuDhDYq%EXg_fF)*nbTASr)c;%C=#^a5m>o2nMkAuZm@4 zFMR}~tC7{RCqdt$zux1{5C6lF%U~-tjT$nGx{MqzmrBqJs*Mo-%3acX^!=e?p3K%W z;XowwYmy}K0bfqO^DWvQ3|tSd6MyKd-nhUNzp>VuLpx|6 z=a*px32{RB4a&49n-(WT=6Adaq}`S#{rJijLYMd`L@q7?mct6EYX<{CWMC-d)vX+I z6bZC33C0Kp$<79wqLC;1-0HQAh(R$%S&?kzF)I4^8-uwNQ2o?)x)N+Pp_QF8qN#!BPHrbur4BJlTB)*b z)^m}~{oh280jtrB9$OvMle}a8)EXaFd2`#KDY=4>wW_zkoY;^K_dUdaDcO1Cpd_KC;H9d;xyqBNm;(d>8Pu%k z?VL$s%3m({`g27N4Bj_g_L3osEh8DL;#pX(qiak+ih>Pc;%RXBw7}84(hg{e1}yU` zSY3>mBwJJhRBSXnto?e}`yFfz(O7xA%FqkFLM%W>UR#L(`>4a~=_j7-d4%*u&I&}* z_nc=(Ma!<|xJEj;MK`>{2+pVp?a3z#?`daUl+_Its^8>wRdJNsO#-Iiag(p9k9M~7hyRMqa$&i^#yb~&<@ zM6@25cGPae4MUZmej2j69EtkLe52`1mcikSwK?}vq;}{6vSSj#Ez#ZfT!(*NeE<=o zQ*wIUF}WkZmxkkLFNlI_x9}!?>u^2;$0pG5I)pNJp4&rrYq@AiAbqsGPVuG_cfiUH z$u2()r;-w5V>c)x3G(addrOJ60RL!83AqVDe71{vuoHW0ToSTYy5IeTc^4*Tl>5t4 z^k|8qv7Yp3{HGAKnZ=Jz2 zoF0B)*__CYlP!Zn`7xUl`;h0h-FCKFH~7g1Fdv~fvt;hhA!i5=ckj%1x zHkhfbJ}Dx0or(0^T!&oU(%K2BB=ugrr)_(#{fSN3KF`rWSSa1Qwj_9Tgv z=L`;{G>|%PshkeVnUmL#8ticzolchQ9J!0j-Dw#8d31zy=J{$mc*N+R46?Y@$1U6w z{#v+Wa{I}dU9RxuiFXz3ZTQ+)adAM{8e`&$_dY8OsX5dKxpA0mBl*-0St9Ermf!0&r1`W+{_IRkF*O!TdtdUju_=2*^! z!2abv^gsk$t_3m9ur9KI;)%OD;6x{J9;~H3J#WP+BM{>)@cI_Sal}g0lWEe~3sJwz zA-Hn1B==gn4SYNTt~~=+Ozv9?+_!ewRC4L0b6IKSXHnSVxhtgze>1A8Ihu;mora@2 zZBCREmnM10o#3cFvv>+$k6rb`Lz#G`QYO$o#k7ND&#QqUw_5%b?ksNgN_s96%Tbae z)?tqhj+cD4uRoSi#sNM0O^Jxao5u_-1fHIOsP4ewlPn*bggZI7$3jd)Uz1-r+JQPj z2-gpTpX3{JJt}R>Wj=Gim$$?$IFImg6O0NKxUuw9+W~~qUk~Iz2}?WA@tnS>EK-ej z_?FL~b!lvp9~V8uf=G_T-^eO5hcAie$Kr|KMPYW?BqN?H&Ba4gHZ9RK%qrrS2S2Ji zCYMi3R931V#r%AJqisEqGW$y6rzCjLj?^Jq*zdb5I_U#?)yB;awc~;Q*@oVnL)UVm z-|fhhJJ(9!FU*ev$MQ+jy=|(hq%e}|00d$k8}h5~Z*Qp`>>Bm7rM3Mm%w;6lR$}39 zC$+6M4;nV)wKXJQid<`Pr;)y9byY#h5_+5q7!mdADIs&T3>lReKt|>xt7ccVW&k0$Uhc_SB_6VG<2O@(F-pda=Z_+5F0pBUU zp@Udi@fe4eB#;u23y44&NhYIMLlTT{!$scIWrnC1LkQ9AtZuErI6hMUei&o+ZkzE7 zZlD6wk2Cs)%6U?qcNIY#Eju&$ZDXWI?T@kRe)$)bY;BYAuUFAYO==FcRc3S=C^i^ zRO<}a&Vztg_K^&qn7KNB<-Bp}!uj5OFwaYw9|8Hk5is9?YcZ3LdU=zA+O@xvpQT|1 zA|$SsBx{`(XnWmi4>xE4+00AiEBk|)RK;dUY)9Hi-vOj7+TZx zS8(CP24Q~+6w4#6YGS5*_H5pdMVmSD&FV59p;vt!cVnK^c`+5jIRD0UH#|Ndop;z@ z6;$Xr986+jWcpWI+SesETa@Lq4&~*KpA@B{3$K`J_e@F0xgKqSz`v=C5Lz&p+U>Sq zhz@c{?2E!U@4ZPJ5i!U-HFB`Q{q+DD{&fPhr3}q1pby00&O>|gjA{5|`idF7eEXmI zRmUee-O1RSsi-)e^+5OI@jqNC=bP(-cngK~%N*msVK`WhK~^KvrXPQ&q5&bI9oi1r zTz*;5206hI{qAl{mK;TOA*Y@z_O)%Qy08-CCLazvHz=z{1m8VikhVebTpQcU6YGms zOGO)#ob_QrgBY-(T_A(7lwxekA@zN9y|aPLd{{oO>p{%O16;Qr_c_p$>Vi}w3X`zD zZdg{Ec%Wt$i#{(mebIuJS29EBw$G_8!kwa6|)j#Kf=fdw?%-jE{+~-&}mZ zPH+#@wvqThEI>FW@f-bhoUsM|WdTJy3@b+CvK~Zw(|xf)A!?j9x5bQ(=Ea@H1zR%X z#@HGgYbFRw#jukv1Iv?Cj`-sighVwk$BB4Vjmn{pc$RwO<}%N8-af4-btHRqw(5=9 zH!h29kTq{D1rJ3+hQP1q0!QELqqrA`o?M|k`)!g!Pu#_t_zlvmzvQ0uNh7mzgQ6~B z?=g%=NpV3?*};GaC|9SCNF1S)CXS=C0)ZsQ0Tf`$>*vsd27i3L`;M;s+Jcu*d}sxE zZ_+j`mQWJiw`bZi{a(F#FDLyxg8eP^S-O`|Pm>0?gidB& za%aR1oM0RYpX3DRMM$0MvF20NHPm|8-)IuyVhOdR4R*K~_WdY?VY}(U9$gJy`+WKn za^alC10f0-i~VyEI5pq6#O1O??y^w-e0Zb6#4P5Q@?ORj8@_XmRI5KQLP=)9BA!0+HE=F$0aHVE>!V#UAH zbm9S6SyHQJ*$5JhG;3iGo0Wd+uL9cf2;8{yXV;EPld`5DKk&*x7SkRnTI)stWapFt1%iRv9LWa2?F8ud!3PfPD8WU^g-gyl|zM;Eip{g-| zG&B;ks{YD86l^h>7FfT~tm`J$^_bT*4gHv1+-w|pkp&|%l7I3?3{x%Q1y}z{%^$*s zK%hD>$IN>ver6qQ73$E-DvGI0IS}pXw zeOO&lo3A+POA>9-JDIyN*>^R#>FDtc)ii;bw7ghaJ?VRIYQp`x2YYlv2!3%66C=V| zGcK#I3F(Hqw66+CYej4x0XxMY&;P^}tP~KjG%ehaY||u>=4&v6+LzWw3V$l9#d~h1 zV$<6qF6Op%S8iZ7#NIsE%k@nOna_B1>q0Zp39G?y{UWXnwW1w=H(#f4MO`at7uM)P z-|6VH%EP@~r$utw&klVO(?&U6xQto73x?FxH8bGTnGFD=OOaV*qY93PfsHo1!oY&h z7Pg6rO_T$ldup4Sclj%?SJ(C+Tvxuc(ZFV3A!R2Kvg92P-c0`wU{=mm{xN@EjVUlx z?<2wT6F2xb&D)%TwK|LwTM62YdZC93A((63UuxGBlv#&VLX}EM20p1)pe0h%1o$+M zkW}ov2?5ZkeROO8S+}4s^19zQMeeWb7(0#{SA>eMUT2Lb_wn#ffE5$(HRMa=hrB#o zktx(E7O6s;5`)9Vcoz1+0k3)x>7Mb!0u&SS0?&+mV0?=A8wO$-_d)NjRI1JK^-9+h zHwXI&mCd1kO8j$zi{sVb{dsb_d7dMnW??aB^c_Qq*J;9JaB-hqv3-%M0I3~^i_BDy z$O-ir*dO03@!*jCrJt4z(nJopRdg};HtYA!GWjH+&K{rjI_d6w#PWGed$2s6qm>-6 z2oB#8lX>Y}MI6;@UD{w0bx9?fNUM%LXb~|@bNzMveE9FI)X1Z;=g>Jkx!IX=VrK@e zc$f~!Wv4RjAST{)tsW9=H<%TP{O%x z1^K+9;Hk&Y+86JgvAw#_{Ok}5fykNK6SIF1w>oG0z;~U0LcjN{V|4r=U;pmxqcSjM zYvMYnbXOPa+9KA}OppZ5*sQ?6s)+5skhOX7tSjwPK2t3u+>Q+OY#o35x9&AN@mkcI zl*9Ipx?}YCDTpFJr4qCX-5%3nO0!5yP$ey+gzCnY-Ds;;ioNZDV=leAhQ#a}h-+xw zO%hjqf0Gpd&F03^f@O4d0X^8C5!fcTkVx^E5x5E|Y%!!9lL5!)x+w*8&yl z9Mm(>Lwd4`H*{McSr2}e^_#E;Y>72Ca0~TJOR_^HBLRqaj(QuH5t92MC>+$-p3LDo zb94hK5ipBf__Pf<%o#+yBfq6#KGg+cP*R(2$&kJqMw0PkMJX2#pJa0i6wGxmkg;(lHeF1Xy8s6u>Q3tj7n2Q`T-muW&T9F zQ@s-e+IY&9hYiU4=DtG+W0n38P*!G1MnLs%l+A>1`qgM%>gS%l$xEEkE%%t2Ka^^| z3w<^PDvnkQHdj{{pBDISoqp#Q%P0_A*@YRj?HU_EduRH?PuhRyf1Do4@j>!v|vq!Y1hZ{1XI&oK}xY@r#Z6lukgVIyo!LiEwEG5nA zVEOrqN%jJvO4eOod#={^B{`Iy6{XIIQSYo9y1TQwQs;TUuSS>mmZ{z^2e%%=fbq@W zi>x)SzU+;Fi3c`{O)SwaNe+-M19mr(N-1 zjtMRkpq}TCqR1j822<#|z>gaFq)n~CCE@Qk>qu$^i{k;pFLf2Q)JY+Y{zRTE79bx9 z1XBG?<*TPPH2|K~YX*(n>biS`zKf0l^)%t~IEg9J$ah511!ijU1g#>+B=T;juFA}> zy~m|Jk-{Qu`oap9Bi}r5P9b+v5&V=7qAUGjM%L2v(ETv*MfSwH`#f53!xV3QMvUPG`07sbgZ13%l%Q4N#G}jSY~?cv zem&n48NAc_A`=Z@cEH>@X9=4NrH;Id-8zmNSCqD%YxPa8Y1=z{_mI^p(o8EN=+Ui* z*s7m1&7%6VbZJdYB9%B$k$a{Tkj{4^uxhOoozC1c82RGP&WYy~Eyg*eqC#QoHLLW~ zR2U-9z^I-ck-rA8(q^eBBjJBw4}I*EKSP%KH&oC{;hs#`GfPu}Xw716NRU&UX@@uD zZIwm`SK^Wya=zx&?CA(|X3deb7uT?dwl}B@T_LSNWc(5?B>t%O!TRgJzp^hxJoAop z&kRENEJINORPGWQJg!WH(nzWV6-a!RjYYJazx6|np#)!V8h@d`H_&4U79N`pGsxYFDMxnZr;u1Bh_oD$`K#VcdoeTx z!15#_XFISvd6kBoH=AwE+iRlzt0JkH_xP!rDKYf{_su^xV1S`~NP&&ziHQ*hE}^}o z`1LH%q*G!E4QlIr6)E0I-BS^e*3HH z_hEK7_eLlE)!&(UPOF5%ROw8LA=WL#)Z`ixFf@}qmWn4f)9~^S`Gw3!v3BG#woWCJ zMKClrRz$XouLX+4fFh~7e@x8j#HpkV<^GP6vI@qEu8k%4tUAU<{vkVXHytvcH#@FY z?oA?ViB_@L7akiz81DE^?}QNtSZ_yX`KD9?iHJD-{vu((IzIiM4ohit$izm_E%U}bMSV7Lg=?`}EO<$fU^S1{G#McX{h}-6NH_HkK(^1GD4;*i zqBPOCv{Xl|B;o^O*EFj6_?gw^0Mz<0-Uzx-Z#3-X3#%tTaf;4|7JT-{6a0-bd*z9v zW7`GZ$G%q!1T`JWMNtF#Z>p)BdGSY!FK#`&qVAEhce~Q|e1l4Ux5>$ywe?!~kVN&} zdXbB|twR3r%iTECw%{1snefxR3u3YUxifIhU&g6!=d#sf=JL{zppyT?0-R)s$;v>( zzvidd`$pEX;|Kw^YhqQ*v+9RZor1}H2|9;Lsp2~hitakOvFQP;vy*3rXKWuN`_3zL z*4D?AuP)llP!%!}z3HDioG6YKCBGQXlO1(zpBa9L^@nRcc?IWMW2L+Er9r-`!RB4# zi#wYB5;B$>$`(ZAy!`WHe!QME75J2iA zKzkIW({>-*>3kvcl44(gM4!5$C0midpof7ZqW}*%SNwe_R}LIWQP-6xyukns27Z3! z_X_}WY9X{c`T2X?sqv{LdR|YDfvC@0w#A|{Y;=|}VBmck0P+;)#by{Id?s%Zc(r3$ zt!97nS;W+Q;UO^pBb2K9SF{YV(`IF0X$&pJczn`sLNy`5zqlsmEapQ0A^zLAp+AKY z4#-bs{4FRlI8w_F0x>8ENvge?4Ls$aJymi4JgUd@61!6M_p+EZlJzK!DT==SdtUgg zR_v$JIHkMEd3Vn_wylDzGxnR}tB510GpFnxs~S)o^d4{L#(iO05Tjhxa`7r2qYd&c zceu7~+4YUsz8?PL#=x@<*Hn?Oy7N$k_p>oM%X(#eYbHZr5)ovK>shv45V8tHL}k1> zjvkxdzv;VR4xau!@q?YV@aA_(c3SaBmV!szT9XYUk+_(f+dqA!mxEnKz8)O9ec|U697UvH3jsaIu%{X8T^!2(DaoGCdk(bhC@n1m9^p0)KTY;v zeaka_UZx^Zz4g8=L zQVz)cUF75$v#R7AnBy$r{S$@wxLzz1<1{d1>;8I-sMzJMIzsnC$j{_MtaNI((%>2^ z-fu(&lD@5V_ganL^&o>qm8MbM4flRZYJ>0BZVD`y|cW9Pk#KVo>K;842xJ9Bc&rqSHsoHV22GED<9}NoM(!IX_+ryZV0wmrV zCT#+2p!B|rEM57cB@FIQ&{)kx+A#7EJ|MC@xwNm$4JOHbm^(zM1q|oL32H;Wd?M%g zrT;W)_C`v7^-KY`e3-NBnX*cZ?riZ3CfrQlc1qAC#kk#kv<$!${nr*B`1gg{VYm|| zy+|un$Ge@xqjR;o#XM>0t@|#cs;pXs?ZhTT8Bc&D{Yvo{{CNKQotF)V zPr?)5uhcSiV(?05cFwf+nlqsVqS$2fHiGa+jeQS!x@z$uAlgZMcxx(Z>}y?Kq`UZ| zpKeD^9qnzpKX(BDl}0byc6@|NO(6G7Q&djoB_{ysm`nr~3LP#7OSWkP(LYw!Am*Tg;-XI0fv(Bb55fpm8xJo(r!#h98zP=Ix2@R#iCH;99{ZHNjmsh zy}LdYF&5JepGOXT3*TR5d%0bU%gx(}$Em$Op$&T@Bh}j)mL4Mn8Y3m3%~|0!-9sa| zm22eCxNc~WL}hl-mo~wf2Sw2!kiZR1gAmE-yS@I;O}&P7@3iY8_zb-~l&e=2qsm0j z>>SICPdpwA;%Pl^nRsU{Bb`6@UfAiY{kARb$y&Kn?14R6n*)WnxW+}*eCXbp=Cm(3 zW$&1mi}0nVq2>TUiOF^L2~8fn6xFH$G}aa$zOe2FX)&XvL4rQMJw3vj>!fJOW; zxXWBsz>{e0cWQl-imwQ$HCarLQ+yIuxABmCK5L;^KjE{*%1!+tgdNk-Y(WIE%=jh7 zd`9}u@GyDF{cV~(Hrl5-lzv2j;5);6mkCh0cN{hqA7Iv0`Z1rkgrN=5u6zgift^V# zQz*qX=kB64EEKQQD?Yq&I;x*{v1#ce%$q-v!3$fuNyvYBVx2d*#@O+aNm8m^8aH#C z(uIROgmPom1`_E%T@#Ag6uv~wj?qxLXE^qPl?F)Ko!5FDl?QmG{y5amxZ&+X`JA5* z?!?B2D$H=RiR}l5p~@dNjIuazJeJZGTl09lebG-233Y!l|7-miwiyBhR_y{=X3I#r zA0BY+PJ-IJE@4F6PL;9ifdRa%GCb=pexynUu%bf}jzO4_<7!yr`!FG=#8IwCfeF8( z@BQ3-x%gXnQS4mqH%U}t^kj5gANH+S#^i|{Q2A9-_N0fD4oXb z$4uED&HZ@}Nt#sN=tqq{1iYZTNbJCv93Ovp;!Jzi`Ns0exAes={vg)<4#|7!u#vS8-~pLmaKI3^rAd8LLlbuJ08jD@X0HM>W(MO3ZqoinMrhHoD#9IEY9ds z@-bGZ<5AagMjiHJ5g0_LAgSHxA{$#|qW=DhDd>4ry!vEJAOFX`v3I=4FG=&7O=t!dhp;}mc{cUIF3#5o>*k7 zhWZOpV2|o^OV$`?$jy0$A^WdPa%)H)k)E}3G%zQg2YcIm<#u%85Y8rerTSU=)mDeI z#L*&SyEo&En$5=9X?%%_9{HDBSzVv*3H;WuZfTEy#;*p;_v*cQP3Cgghdy9_ z;Of-+!ngSS_J=4n>B#wp66I~z#9EC1f(Iu)_wTIOXY-r&3@iPo8=g)v@szm!C2Z-> z<6O>z^Q7YspJLl5zN-l}W=Dld+{^C3XYy>yBr#JhWwBkVYPQU)vyqY`C9NF`9Fp0vm+vpSk>Gb$)R1lmFh)D?W11 zG=S32M0<@xL!;+ zCg1qgQ@Y{+pZ9ykz4%*888=B62fd);(Ugl$FRvI&uupYxRV1!Xt~)olw$%F zn+>nY+pDmAFgt+uDbKqWl8@Ac7Q?{${)uekGz6-h9H+ItJhhuwTWH8yG>Y-Cb(EM{ zgRT9n@TGSh*~&2EadF4{AS6ajS7c)&Of&Wd-rQXOG(WYMo0)0)=ge_-CvdhXPLm`h zgPMZT!P#b3L1ww2$9TD@|lR}~0Cu8=GLIAI6%t|-peOsbwhW)}& z?)Oo?f)TABWTtjN2f1GJZWu*!4?sSJr@9mnXag${h1FAEG}Cjtq*Ma#1@KH#yz!J z+ilG>^6v8>%a!w8u8HckAD`J9$qVi@2cpfyZ>h%%nLV-QDvSMjv3cfRS6XQXs^Z>4ml8z%T&!uU6Zf%lSl>6eqVPvJ`C;n|rJ|T(Eas(0 z*^5T)?H!j(FWTK!x|G`Pl-qlpN2qUinvVy1a38jx%+sUe)1Hq!+V+4tUelDinedv2 zip3I&`o9{v+O1_D-1VWdS_pZ6aW4*4o~xLAaysxzjlaE%o9du4O&Ee7>9-m>l9hXx zLw{7?uOulu86VJ@M9e*rzNgi2%qS-HBJ8Om%|7${V$Y23&==pVhW&$3r58@1f$x%N zUM0pwL!I@$7wdAXT>^2M;q_=EnvUGJ!9!ia)2uhjc!NP%#ZRXhp!=eA#wV?A z=;x$qP)TgZv2t{lRq0s4a^+ZukJdGc8u|rGCevU8__?7xN?xgbhn(5&1J$-4{?@4p zN#E2;dNbFYv0CwW-1<<-SY5dJc4J1sTDb4uHcFcr(y;z7&x4%@r|)|l*M#?v$xtc0 z?+Rwq;&3r%DZjOj<|z~<8zCc)$}-c9Yt8&v4>&r_jeX=5CceL47~Ks=egHws_+SPR z1u&RBr_m^9;ewv|ckrFZ50F2IA$4up%rpsy6Z}Oh_s^Q~Y;w<|U)_939e5qHBkA4$ zl)Yv{(B-!skN2-jNQO!h?Bv1Oj02f^w<7_Umq8j@r{%T*@38&pVxjtf031Q%zN}Tr z(Lk?lfr%u8lYNex*SiHvdipd;S&$7ar#EzU7MON4`o`0nS=`6m>`b7yfRqBc<$7 zg!48bNa}rUuYtcM3AIe)#XLSH_zhF+?A|JP6}xBrQL!3RK35mSX>ADOv^ALgI%7UT zmLIn$CtqdNMOr4Wt=8oL-%ShbDv z-d`;*8ldjCZ)|fRdv6UiIKaUHAl+gBA;s8i7%7ZzKj@W$uqKNf6;0AXKwB03Tg5JE zgp}J!4iIRi0ReqiL6*G#sIXA6e2%OxNLmv^;iHLx#;5afL$bf2_H(3=P~ja2b=?kM z&v#LL*&)n(3|PA}#t9%0aLYbmz?^afd%uZL;T8lt%w>`(YXs1-#|wnCsetGj;U-2E zKnDP6+V?8OAOr>sAdqNlpalSi6CYr4lH-uIB$yNYgF=X1Rp3BOA^ zbIIn&Uz*3*UjrAN|15bTrl7$iei{9u zs2p%xp&)(>V#uisWW`qkqMwNh_MAl5aDhwA=G&t{G=vHQzrDj z(tUHQDNh0%KE~XZ2Dz;cPIZ|Ia0~p2cmc^i4^=s*JukzJxL~asSk3BP`ghCz&7fb| z>_4oPC{4vbeeAE1>ixHf_5SH3Pg!`a-ZDrNA*=Nz2zFYGNa;fe^?45y*U059jwlN; z{Rs>Vn|B0(KJOvYV>`lmn-St=EwkWnb+Kgkr=KS0zv~TE3tM_=%>IIyVqZv~;dddN z`OT3&E#UhN27la$T&MVeAJMY>ltLrJ@2UcRPZ@-qKu^!x$=FG6o&qnuPxV!Br=U%I zhDIk#Ia2(wNjJOU>CbMj^^s^vHwwB%-<@`Q1oyJb1aU7`Vc6|HE(i=~b_eDd;=JKV_AC@G~6BiQpz zWKKPZxsS_g6t|`b0PF%TxeJK&eV-=^0v#8yhJ~-;X%eLARe*pq7T8>ZeR(yv$f@J7ax46!EQ4Q|{A(it7x&vFyfeUf;>|1A0$CC%&{FQA~U$pcNYd zzlZ9>tI|DXh|b14%Cfz%8M2+`)cHkZNI z=5qLm3(@vUDRZnmpnJQkSk^Ys=5ltfqR{`+eYCn*-X{ez;#1IW1R_+@8f1EW1{aex zkl+A>@ftuFEkBdG&elX#HE;j{zYn_LQn4gDMtK=o8c1uSmH%5RMm>>Ao5wEM6$tP- zEdh-mz@J~H+CiZ61Jv$`nsWP~kDha*@w^1e-}+LP6prMsMQYc_5bE<5axXm0MAYS~ zDn+EgTlSQr!_>ow^mrM;F6+1*MVlj~Ku|vkbZEx`9W&8g0lGL)Wk7(b4jA-k378^Y zc2v-C+X|>A<^+G*5&dEnIPiXi0PpoX^9%1*{&x%hZUDdx{%OYCzcc#RSe=|(%gl$e zFDiXgNtgmcqtoQrU&px!7vF|yF94U`4MYb07m;2&Sj8DXt)OVkFlweh?zvGF@DZL6|=w0BSC z@IETTr!xLvnJL#Dp=I^sWu~1zQD)$zU`LzoDCm;5J?$xIbt(LtQ zES1P>q*EX2v;g7kB`kcz3yw%jA$J`@x$6@`xp8y9yxC-UeTjsk}&uSoRDK8qz|gQ&d~R zX)CB!0<|`{W#9k-ZO;N%0 z;BRLCF5CWC^-s2GyMKC^CiGTfsZo6Xuv;R274;SH#-OZ=CMG}(zTSOx4#y_jO%#1 zfqK%R>?ORJj(XFfE*lV{R!IKs2o>CmVBv!Z6h4MP(K85?Jda?>PFA`LmhC~PY%fA( z`w;B<3VVny%U(vXY&QZWI}s?^fk5$d2oybuK;ff&U!lT#5T^H>q;vB(F*}|fH{5v@ z%QJ^MEkdy4d^vfMAprsj>;&3e$t#;_&YnLf0tVD}pp2L*1_X2nv~h*NsSaohmr@j8 zgEbklm9Bn!^_e+UfELGQ|4|U20|5gB#H}mtpeg~1B*@biUFB!TynvBh*&rZBpeNL6 z3BrY2k=plXs5SDdxQPJr^?QKYgAX85_B4W7OIXd4#D|Dz$j2t{3CJ;Y0d#f1I>xe~ zL^8o%tpKly06QIExYG&Rb9xMEdh0{+Z)`Ch5l7p@*teT0gD7R(Sqpf14Q1w`E^86Wy9MEbtq2v~hhWj8 z2o^tsP}xqT^w^8EzCTB;0dJuGkhjoe-{<+-S;BY^+g0rckoiCV9_HC5<&&HBb2`x;Vw6Gznu0hggPzesSYZi z(;8hO)dkwjkbI5i=Se;SwKUQd7$7bI1A+cZj=5A;aJU`dv;uT4KntzxZ&m>uVBk~& zTE(gdaqR$y7oZaai?vF?Ac%Tn_%rjdK-C-sbJrlH+v5licpqo1JBrnc3&0ZrF9Z1( z96_YlK7?}Ca+QQgg>qR?U-_E&^>j!;2CQT32{6Y3D_Ry*8OdPt+)!=ZwgP_QMx~vX zs=$Gk#Ne-g(Y-eOB@+E{Q~Psc{@0FQ_#-s(U$p#RAj|WVT`~LrT=?71MlkO-r1bp_ z`p=T0KMnw(5^L!dN0Bn{4~Uf9&qTo>m7po3WI5lFO3JrStb7~|8YY|El$UKAs{}+# z{aWDHSJK9n_!W^an(Y%?zn>!S4bp=C7)h8Td06h-pUr`fbmOUv?{6yuez3zl1UoH4 zsPjsMx~xYq|26~*?~|E)$ukIb+l7=~ucOw0-=X2~x6o|tKhWmXkC8oPKT0qB2EDI3 zg26Wc6P5$#Z33>nAG|1O$#b%OLS#ykuU-EVaLZm`(>{4gPHS)J{ic=+2eM~@xsL)@ z-2+VD1dLw}44V%Om<9B_>?jJ)KZLAlU!m<8pP}W157A`IKhR+4TMR(b`uq%$?t2g{ zdjSCgC`At|aB!Pc*~?ueKfjLiSagRrJW^?$wmounfLb5*$HuBT4hSI7$b5i~TI;5@ zsw`>bKVNf$6QH*OT4^AlZESoqx{6rm1ZWciRKcO=!+e0V5uiN+L{=@i2Q>%&7TM$s4AelY0@Ts80n#b~ z0*;3WSnY$p>9jR)WU>R5wIAM6t9R+&aW~e#7YWcL{sj%vof2bP48>2{j z6QfzaAR=9x%On9W<2ugp16hj^%vsGn<#6F02o^ttV99d`cYg_~eSe8Mga3&1(eI(v zsh=Qw%Gc^<(eI-6pg$n3@2?QdJu5G(baRBEYNycufVkO$JyHa2!spfZLNDtgQX;c0=_o{p(&C(I2mcKPKy6*ZZ%j zWhu4eg@2;5zpZ4z<-nS&GAk+lE7B*(r9VWjD!dz!et*Wv%ViL90KkUb;VhFSxDkA@@vf#wrF zL-yI4k zuW(#VTdZ(yzde2lP82|>|EIb4)ecbD2W)vw`o_KT?*tCc-6EIf^ttLNx}0+mtxo+E zjYs_xwNH8zk>0O!8zD$l0$S1Bc?ALl1gJ$oQwHg$s--&p<@?T)9G%tpW=1?0AbQzS z3-&GJ%l;hnorwU0`ayaDY!c0 zf5e8TWhsqMgTM7foYjYl?nI>TA93pPa+$rVK(u~$IU+;%BenO7oHYih|KIWw**mU( zvOKBQQ~-d9V+HYoGC{2c5Vz-EC-!xUX~#PvJxm6p9&|T(-%&!}Nh;ePCCP_0FWz{v zoEax6Sq}O$5zM?1;q1i-=dM8{e-k3b_aapK48q-CMveY&pwWoGqt*D&QE<*-47e7U zx&fH`gv_otzbe(`coW?YIcFYqC5OE1Ibi8?YA=Ec;vAHBDt|$%0z5Al*f?Ndg%j8n z$6}SzSsy`*Qon)fPwye2oNlB^zpSS13m6q0xqw}>H*0)FM1ldcq?f+ z07VxbM!VBLN28JdK#hLCLa6Ln1oE~b*l7t9A319#31^<6@1sPVy2Qpf1MOpiOi{6> z*2d3mV_=7%2%}js8=8A z;2nD8+J`zr{(vUqK1BA^uh3^^Ii_xu>x-$RPJ8C5KTpA!`u5Sxew&ur zZ&RemowCeKmrigeAh7Uh;Ks*+IgbL@KU9vH_m<;|JC5SQEk|+AEk`kJ!(mKWcNk}_ zIgBZ54&%%J!`U?n|OIuGY){UTlF- zv;{Dhr#0w!4P?&aszqwwmk=3x02^Oaph^LNGdBR?zJEiw=pF=U6tKmGvN}Y^58WT# zFSj(Z;@beknPC9n=mOMtsRX2H-$hd)up%;`B?4B$?@xB{viAM$8X?3tRp4Oyk=<*~ zw<6ZR*P@?90HBs3`eQKvnS7;Y|0&>7`PS+G6B&w5>~umU1;DJE5H7q6sr}zTud9z@ z?e17fl>~vhU0IHB|2GgRx|e&#{x(-hF&Yw`;a>4cDzj3v|Irz8eG8qonl_Yel=__> zzf_DaIbWd9)n8%U!b3P~-Ca!Zmo#zvAKDAeZ-o-{b#bRF=Z`~HR)5NbbAH|nTF1O0U~Qv@Fak0Y0zV^DqyBs5Y$4e0_cRmdLCne zZi|Avq=FMo16TlH_&+Tn@WeLK`oBX6aij_yOh2+$l>K|L{@wciiJ1SUID$Q$J@ok1-grl>he(PX_{Ye=-(<3! z1?O!4N+$CKJ1sz@^IC)owjx;a1R_0NL!Fb}M&r?+AorYaF=_!Y`(c^gQ=i<~BzFuE zPm%CeYo&Gy_LOzcdmNbkKshejas(5W974}44xsJC&romZe-P^Vo(kx{fhc(u{*qk? zlst=I=@ST*J&Z`{eTWp_g;4RG2p4UYv-|~{5g{iMUjAmJ6x@oGg4+-&xE~NK$ADR1W8%s~xahVc zn00?S=06TBdPZjW6y&)ju}iinoYqdX9ig0FH&q1)2$fy3AHMt-m^?rB=Ra^CR zIcH~!paJTldl$6N{g~T3)uB$BilZ~>;fw(~S^%S@U=^(mZab3&mPoQXWx$^?XkIyg zu1;%(CMt?%lq2n=H@K=5=r~VRtj?6jD(|Ln^Z*=HKxe8!=K?hHAOzM-pbL;_V_+gd zAPWFACqm%8N;uBi321YL5MNh;1L;TbioUpRlD}@Uzd@t-@0R_W+x}`Beg8%(`){iC z{@bfkdyZPZQYNKVqhnRmSbmmf#fjmc*7s$kjXDsWt6rwc%5;Klc*;RUdcJ}PjmTv! z;!KQ3#~P|tzXU33^-*NNu39A^U(M~SHM=Y`Td1k^b(PF)J>#$IWc;Vf{(kdwSYnrC zbE*Fuq@D33h*NYA!e!f$+V3}LIPwEzO*w>7H@c5|{HPZlpcVxImyFr$fs9$afQf(3ypI1)ZH!>Y8xhKy zk8svPggY&kfhk5zKk09E+X;mOQHg02xP5bLLhDGV{HI#0~D!> zjkZZJy5-OLT#Gd?j4d4EV<1;sN35$j=EX)lS<|7Hd`uGXiB14qtR#WgBc zu6{rxS#Fmjm-SBl7PSVxij<<;5$Jdm0J0rQ8ZnM~=F~%9 z^c*n%DeBpmJI-H{vMRszX@OQD+rZ7i*U&Uh!ozzWI|Tq%U;H- zk6Ax}glQ;%x4#AfCC^J$6>6;nl$Jlu+51~ehrh+S(h1NsKPP$_^K)6JfN#&c1uj<7 z!!vltJuCdRpM_xN^->l$izJ7avVK80?`A}bwlXjXmp%o5$sUN3-*F3~&fxzbbMk%+ zy6zAz*z_%f9GY%m0^?4(NQVRHHc5QE;Rzsp#NXh{UJGCI^W{osY8yF|9?_t#?OKSF zUG5q4xb?&(>KG{0RubnM(;nSo#{_{vmHqpgO^3hD)eQbpita*AcvV$6TP%Kmw@DogAA zcU7V)L*-0#lQTKM1SoiAFGr;KZlv|ugZihuk3RFtar3VD5;+CcR+A4Q()(A46y3=x za{*em((F9h3ZNiCC3{{p(=WD$mc@a}ly_Ap_#UxwJ^~GO6sw=+&(qIuYWw<28fC?Y*yM(x3GqxGp@V#tla4Ub78AC-QWZjX`poKnBTw5R?!XZcS8SKnKX zGgf_zybBMY-pEhj>-jf`k~iQhc?ltEB^2LChd=d4t7@+6!y2SJhqruIYYOA%;w zp$zKH&xXI*R7pr|JPE$W6X7F*V*07@rB8s5DlPPWvyX$X;keiu;A4E8x*bh7oWf(8 zatYDtJOW2qFlDlSRxmtI?f_u0aix?i&b*#04qP$lvQD-eitpx@gs*frM9Hrqy8Z=q zhJ1vabH2o>E560G50=Zdx>_aR?DYo`Ed4qBofbnhJx37%`>W4WRd-VLT?-C~)7B(M zR~8SH#{=&gP{5E{2`SD>;t~xI>Xo1&QrCgNAnoJD#CQMz5CBO;K~yzpc`1Jm5n}&$ ze;R3n-oxpu%jGTrH5y4ghEvv+qwe_sqSk=dky7#?!a1v0?1_#i30B9-?*O+nx~cI- zC3Wlw9I>83Q>_Y6-;fD*TN&)NF|aZKK)7657eYLc?4V|?@^*y~;;Slf&?M0l1${5k zAFoxv`nq2?wLiV@@09)>;-A{TPUY1|L2RCyf^CCQERw?+Xqzh$>Nua5(xjF>ikbs{ zjs~ZEi0o^QVyz}*vsCqnFS{G4Gwd^@^mqZ`yp1f#LK3zw#| zOEy0X_wt5lUPV5KNvLy#hZbX2-su_LJ1DE1aLiHY5me-ce%G?r)tMB2{p|DAL1ow@sb zZ+1v=HZt>l_tOxvklpw0Ip=qNWi9W?&HP>Pm%WU-gZ_lp6AokSYEj&8dRhFv6uiGi zlG~IXJiN=_1I)SWB!(E~@N`%LU&ee+ z>7z-W%#ytH{c1jy$z<-P<78nUE#PsQQd5QrxVqsWxH&?2pccp5PoBQop@OM$3e@Qf zSN*>7_o_JJf%5ChNMic=vG2hc*6O>tIhf3r&g9PMk^Tqd@<+{`mt785!=UVd?%QDY zyaPtTE_e!_fvfP>Nb2`jashf{s=B>vtY}p|+==&i&-R|PanbQy1?5U585iSv- zI(2$UhJHW!bCe`aV%u{v;hCbL;ke@AqUfp=LPCj%>%jp{rShNU5q}5z{_*60mT0WV zxiRBv&OZniJjg4b788$Q(=!oY1$tI#oik`X&~);*s5|IgB=ve+AV!_9V+TRxIJWi557km^x!ri+>Op{*{4H)9b@B2@Lf z6G`RIp#HEwqQi`1xMG(qpvuYafY84abDk`3g9H8wf6*S8-8RG9X(>EyFBQ6RDi$=c zQ8A!PUuDvB5YGT{u`8Qi<=DrQ?NqqQk&xi?4p*}+FB$gu^L8RoN)o<5q1}Wd7_(Ls zda}=@3xAbf`1grj{(89!rlLP@M->JxIfnGJzJa&=Be;ux1z+Jl7Z2JKL>q=`9v3Pr)8pGi`_ zIQ;M0QD_PO{$lyHdWo-JM+-NqZ;3GG)yZht6ij5NMqV*cN+6*G)Ks;2eujGcn zB$T7)y>R91g)8TExN_fsEBgS9tlPK&qZJW1XUgiiPHE)tQm?o8`SN~#60a>78R1Cd zIo!yJ0HSk5b4XuDeGlmw;O9bbj^y*fQOuJ6LB&6PCZ{eEsu9e+6G>$+q5kl%FzYVi zxTsJb<)lc6K>zDPXf*aqBvrnKK*0m>ci+TKoxAlV!WOu(G&xe42fUi-F4YKjs{p4a zpeBbvCtm<3a-e>v#QR>W6{pJOa_xwBaZ7w^{wzB^9sgp{{5$Rbt!aPJ0)L`F|E&3M zu6h1kH$!d)x zS#`bWB>a6ofIt5s<^vEk>u!6Q&|9ZX6Vp6hvU(;x6?}%c$BO?hEaxL{{yFeuEP&DJ zIyUa{_PiTr@iVB`|F>v8;SkPRACaul-(}Ul$ZlUXDI|lVz4n3!$;Iy&T8%pdciE?K z<^LK+!Bg;4)N%GU7zFEgULm4&+g`~GaZbzQ2;C`gQ?a5+mj14^mZ0xjeO2r$L=onx zRfn?kL;w=O$xL=r#K-R1KQ+>vssHEh8e_hj_G?iw`>af%jtQT z@Jk?W2Con%i6%=S1g+0ZU39IglOjUY3FcspJ`+)Z69EHF4dkEz1gunY%CdF|f zXgx<1^|a~Y!soQPiQ+V!A_@_c`(}xWpQbx1LRr_tpL-X~q9>8m=RGtV`wfP#5Pz3# zr1Rciozxfh*xw5*e+-zuwF;T%e~&=rXK>}e2XFqfF!LVZRRuHiJFQ^EgJW{Z$~gT3 z&Y9=GR~WSzrWO1?a(y9TZ=T*&q0f)(|vM5y)etvSI=^JH+s3!eOf!f=@tGJNY}L zNlpi?A2=SkRDKUiL<7V!(vb|QdpoXxnY$Z}`n`?jXZ#syLq9_OK5xNS@HBkcJK&=o ze8*)RyyT(@yv0Sl>fy|RrV|7@Kut;$5!mFWz^3$>yq=<|oql$K>|2pkvKL8%{)oIe zC$Ra2P`K!?SJud?Nu@)Q#+QU4Q0mAp5g_#nHIJj3CWgWHH`1`vbYojzM!=yY0M%t1oF zwpTEFo*3GGigwMt8UBJt5UBVCQignv@+(f_+Q$XdPpb%(_pWSrRpDQ}D}HETP^Z>CUsap{tE>gh{N*Nm$ z+$>pyyGUsz6=+q!Go-Dp9&6l2JL*3xFMR#Kv;3NhM)tLlN7?0)q_e%nHn+2E4z!r$ zfHKi35GY01O|devUvDVZFmA;qXgrqJ9O8Q-Q4ikLOBTOQ(xZl;;ZOnlw_Ai@-V^9~ z-q%>T3)uX!Si?-(2(%f02+4hZ1EX+1OlkzOZV;RiiY2Bdj8h0_3PYzBGdQrx)A|yj zcF0%=qtgobyKO>J{@n=l+QJ(Yhyn2CqsOnWA@uzDJDc$jpN^ z>rCQP3qNnvf(N7)ooI}IEBtR*T*OQV*`JNupG%Vd+^uEUk4*bxyWc`_p;2L@BF8eS z^Ed*RqF_l+?k{>2N&SC^9y5>fE_JQ+yjPy}O!~VQ=(?Z^3wDcUV67HRL(;&NPXe8% zokYEXe}KQ&v+TU)@39qTS3#yTXx~g(^c1<CZz6Xbs?QNKYO;-6;t6HfYTDGO0Y zN&3qc(^?(L_@~`!<8eYp)oP9~n#tOTK;gp(_WuBRb53$0S8%$To@mM^8MX! zgg=Lh{;lxm-^=Jjp!`+T8}J9T8uvY>Y!XF;zUCUu*oC2gIq@&p36x%O9LWQ}fVbpr z1PY!I4tliLC$@V#xq_f+zuCDG-aj)UQRGR#GE($8Y)C=+-K!V;A58XbB8hh@=bno~ zbE&{kCrw1lhEJOPues!L{Qc@QtA46A7U|U!_ZT^lNP*EYf)G$cpb&y^(2_2{)5wWb z=>0hn(;j8?6OfH%y}odhEpPf<1aj_0uh|E2>l<<{BUeWBvmy=&aW*I)A%E_1G#vaH zf)%eLQ1BQ6`S&0|u83sj)cpqWe019kfA)3+d)|&f;X|x6soVcOq@DRSlRqhwV(s2Y zA|gE-j`W_-5V?p5Rl4 z{~gKw-s6kbEO`e0qP++fKZanh#~I4?SGIvgbs_$9w@5=5-M3!Z z)RNY}a={5C4f+bMUcZLF@KN|F!ZwqLUCZDjqab4M6OoSyZ%jaxJAHz8X=<&b?eZ0@ zZ)%B!hAj9xT(3{|;Xv!ob2riKV;gBFRWoX81ed$9<@ZGUce^A1tZL0dGx>K@ zje$f8ysaYXd)&zAO+i<2!g&npD7)Uy5wCt~hD^;$x?UyRM38KCF|S&>O!`!~3hs|s z4Tq<6=^{z1oC|LSGR`@I21EXW)k`vr`-t-L8*a!nP2L%u@8Veh^TN#y_l5CBO;K~$r`(D#r$Jbyb0W-~J6UbwZFd>_IdG%gaD>dYD3#IHJ)}oKd)WlE zwwMH^YE3|_4S=0Vkdrrn^`1HH&u0ljVD0&whSR^@i|K>_Sa<(czyD|rfYt6-5Bkyd z{_SMYkKFr{WQ^@ohYGemNx@oQBAny0ZiGL7HQwAU{oXI3&cNTG!KlNSvm?aQ-G&2^NLyA1JRf4X^^&`QlFNY>i;?Len1iu4XNR6B#sTo-P1Tc2>2{at` z4O}Jfz$n}!-1surGuMD&esatsOX8*q;+I6e2K}nT*+FGt+f5d^PNL|0WwyOu@WYe6 zHi_#6-rd+z;B{GM3S90K%de+sX^r~tt?Bn@zt5d&X)YQ^{!ZbhKt~RmOOz-bE^kYD zB-cP+AE>oP80}~miSy+~NUoQt5YqRM2!;*MVD`VS^BM$;UP0c>Be?2;Ds0#r$&XOs zJj!5*8a$Hdox2U_dO;N$jXH$XGd@Sk@K4cj*r#YX;wz+%I*2w?j-l_OD$IWnxcOzV zQc$Ukdhs8&vI=JZ&rz@P71Sx;kL0qwsN4H_&If2Z?g-mK-=Ha2R!arTlHJqx)X)GVioVSu zLf}2|E_{iP_?)KGe=Kf)PSbx)(m$)?Updi)vq-Bp^27betS!~-xy{&i|Euv zFtgSpkiQd2l^-DMyc1mfXEt_A(;q!@laS zef)RX51hZf3a!TrQjBHrD!*_oCaDjQv;2+Vg2`5kM(OtX$}<1;MP$B)f6rUVrlAyjQpPy&6RnXoDBDCg7p!d}$ z;jR1($)(RCDepl9^X_J;Uvj|%NG^Q>fxhn`WehbX5$=kLOC}`X>RmvKv%Z7B*X!`- zh-?dzMUum!$hH_RBd&`@)5G$>E}~gvj{qwlAjM)8>?8|xiVe1U0$A^vc;9)o>ijK% z0>uBb`t+Or^%G0+x4zs?u7A16^HGIixx%4!FZF)AulNVjQ8=ui&*?rZEzY6FfYucO{M`2Wa z0;Bk41c+GJ?M9XgdPuiVY61!MGs?lzy#ZPpp*HP@A^uL{`{h;p{8kaNUU<@Ps!jQg zZF1qQgNEO;n{F6Vy_Z|^@Ksq_E>dG{hfQdW9KiP*{GuBba}GD1rp+6KDecnZze)G3F4=UN6JQxtSxeg{!3uxC*-;mT1ZVDG&5o zlLM_yfQ|wLHBw--tbm#v0^@z>)tXZ!(*hHpIDM`n|Apr14I_2%e1j!b3mr9+>5CqSb_>oCZgemD1DGg-p%BdAEiT z82B~(6|adfnI1R6?79JF=QW(Y&dlu04NU3_=G}u}uP5Lw|1DBRe}`$?s={B-y1QpD z0iD4wr#ZdoD-HsM^9;J?B$5Yx316>Y!e6i(M)r32NVe5(0lcj*7G^yp-D*5W?)L{s zCVz<}vE^v&{mxQfpP@0iTgvaDHtD-+n(`Z1rn_jxTNQijNwyqoLty>=b!#|w%|U%j zQ;=e52&`NLos&R~f_1fE@tL6SA+1daO(BUX+e(iS7R013>A0NT8_j|T;VF8R?UoDX z9>cQ7LL9SfO&(mQv-#E0y(iSOBv(OW)>I*R$UosLdkaa$k0C(eOx-rY=(>ibeIzM0 zI3U@SJl@xJeB-ASMTar`WGxm^FO1rI@-POQ*>tPMc4 z>0gKb)ijnNiCGf*=e>Vl0b4d1C*0mR;cpd(_5{jbL)w@lSiD;>>{TT0TDixcaa#!f z0sn-*_p31T@8?_qqw{L`+F!%QIi9u)Ir)#FcXsN#0YM66DS8lrir3)n^C{X*JHm~? zb&qSsUvCEXMfd``p8)d?4umlKju6^R{0`po-@z<-3Vwq7yRKtfU^gYMd(Tb4NMYxkei3sr_!;TG6-L2sczV5yrlY>a z1vgh=<1-;%?NlrN`!vI;{gG!xJu}xl7Q$H@s?cowckopH8NrH|IN--mo)hHkN6(`2enFQ{bjh7z)85XS#Z_@Y97HX7hKEto4i^Li`>zr76a~ zUi9svUhO7(yTWe$&pLvWq&0OxOIVB!3ny8AkN#_RC)OsxZk15q=0xKkG>ojDl=6hW zudF7}ngD7(iFiqQ2}C-jgvoY`;G^i~+}q(VejaA!XXw4)1UAU$A&ds>jTC=+9+o@? z%)Speds_%O3r-?s;$Z{^e}d%xZz8E|FM@gZNpxyGBMzR7#q2Q1rc?B+ww@=R+l)nQ zBxI80HUD020#ZjEVHLpo0}&K}d;>_qGVyw#-r!H*FL<2EvOZ1^oGIM`N6IXOGU25V zmIt=ik^>bf&?*hIssil7f=<;YIME2mLny$oco)Re?Xxg3{X2#K#KQi!LjQh??5~bR z^RFf_srP2Lzb=A|#b!Tc;XoF)RweIrGT-UG6=u<1j(wf7QCxE2h+J8SuY53sI)e|v zU;Zird@~RZKpebCy+7u7(C_whhPy+{kO6oy)R<{lNa61rwM5myY)$D zI<}-$GCqyX?`y9W{+1T!HY>!R5~c9l!SZ^s4O$AjT~QBME_=V_*J3%r7*4E>fsPPZ z(Sm4ejrv+MU_kSx{ z(-GdtTG;?lW3c>5pnN^hcHVI`oBjw_Yv!~4! z>~BG~(#lbgohMt(Ws7NFr{$svD7Y6!**i!(^Ls3NI3#4O`y%93ats{26iDj#N0@m# znbY9Sm=70aTd+~k2+;(D<$*FdNOubKNaAQUt6+T&o5TxHl@xBk^dQK)# zfFJ+!W-a)^$N9tx``Z!!qUrvv(0?O2v8G9Rn%u#%Ah`8RyRi^$dQ8{ndvka z$KB#WxLeH@R;}dRMnOVk-qLdil8W}AUjH}HVAMxwao%AJULRs@xKaryVjv*_boo!+ z7DD}@-@;3~{`~u3cH6{LKcN@w&PHDJ%P{(f!>xE!Pjm+OyZH`+zD^5pE1W{)T%1M3(y(>I`nH4L)~{} z2}o z;LqBGK*2*WO5Z{2iQi+@laVz9J)5gorWiu*B~=Jieh4$~K1MJ+ZB>M@k_Yw`|35i1 zK(`BSX^{r%guuEstALtP1D*H)@xJ?N-T5?u0{nQN(@^(sJ*O)BiPT0lak4W^RDLU>Ab@euuo-C)rYWwZ^okOLoO$WQ%$PM(HnM zN6BYV!Sj%MbPY&=oAMW&747V~TpwlXD0EiS=hrHSczK_)LhjI2M znI?FkI_%E`uct%kzxX5q{XT)&>uJspFvyIB{Q7B%qd$id=m=6~tKJHk)Yel<`7*0d z{GQet)Sss9_4T6P(5C3yP5DU{NZuOr>NMF~i?}t?**wJ)t$~Q8QLt+aVqGWLp$Jw& zs#*uM6g&&|Q;^T4ik_|~qNnL+>1jTdEtxskhmol#;II4>^5!1L+I^vjTu@n19}vZO z@Y)cXob?Ur4SW}MDxO8Ka3^~}5I2PQDWnK-x4eX(S1$bOd8MR3N|~falFXsnUMUEQ z-8LbZzZ2dvlENOvx~H_1%l(m5$&AUz5h(i=%-p-V$#s)5fRYJW9ym-OQH(-mi$+vQ z0~H^@?c@^Z5#kc?f5CBO;K~zPB-)({Xopk?B zX@6F~e_i&c9RFHqK|gZZqRWR0Bf;Xd_a_#1+j(3_{S=&1@hh~Oc$5uPuHPTg`m=`m zi4Z!Ra}?gPw_xVq4Nv9`@TlTX1(>EMzDOsrlu#u2i6($AL23ZnUdA!6yaLF%1xY0j zBdPB%(Qx!ZTyP7p?YU5d@vjmDpB9Zr*9(uqU-1V73Lk~9JMj&!WkkW9e!fU5q=?vt zgCYb!-t%W`g};MEhNXyoO|`Mwl z2!$Q@AR6buE{a+r0VEw{UW(=u!01%2PzltLK~&_(NU~HS2~x;U);gH^55QOcE;1$` z!PO5%+$Gm*#L+D;024Nc&|uU*QFp+*Nb0p0fu479P5`m>so|w?DR=V=xIuL_nG9Fs zi85Vql6YpDpASzut*+*aCZIc^W4quf{XL52p1=*#|6;Wy2ND879s!L<9!8+_75KAn z;{YRX>v>Wcpdy4TM6+Y|(BgxwR>7&3XV7UCV21+ODFf`dNAbQFYsE>R06)TqrvHPr z;D=oc8UT;O^lvTv>ffUke=GE_cK_1AMeY5S+aKxMX~I%^e@eC^#xfD}a&AIW#Y<>9 z<{&fvH(G*Is0rvX<2c+UAHtuv6K1y?1(eDKcqUu~_Y?EEiM&KPw|#W}&km)r?#d9FINx>jDQm)!?6JM%mEO5cXR;C`57;oJUd zcqy!hrnM$#ONdnlvT!tO&j{SF3cpAC@id)PSX*t^gcGc|yA*e~;##!06!+rp4grc3 zE7sx=oR{M61TF4Xptu)zPyT!d|4GhrkbPzEy`GtSX2q0O3%Klf(%mgKhj&)vLZ}R< z(Q10}wb-G-?6SHA{IrMeUbHsg~`i!e<+8 zOkBwqwcj_o=PrK82{r87<(m0(%QNnOq*jF1CCw7-_=BAjX!uIXTu#+5$__$yb)B6XkU;IRad_l278 zO!$2MbL&jBbb%9+0l7JcgJWY412VdZD=48A!B*n9;bAZna{ zb^Vk#MhRr9Z!zu-1=`;0g_?`ofGy#z=t^^?&Oh7&iAdYgmB=qiNCP@8ygPv#_ZXx@ zTfc?7&{^hE5yeKDbyX-F3Iqjt^fIKyzH1ddu2E_uVaEBTyjFy!=WWbG#tSdk@Svw# zAm%yqhEibN7}Urq!vTj^%Y#_Vw8V~oCDwZxT6G66w22Hvl1sJ9c>$(^G@k!e>Oqe*ua@8&;_$K)gil`HWrO;~7KXlCqGImN(n=5}k{ncRii7Z*X#UwnF*oHysDu zQ(-jU$?&`7D#)_(uV1u!6W#Vqvw%{)V-~vD&v6#(o^9mVdtE3yfyql7YaYA^aDIg& z7~d8QDZJ=UQ$;YTv_Y%-f&;@n_)8!=fER;%summZ5Px)tz1%zh?D?JPKvshta1F08 z%-{9sFAjt6E#u*xSvz+m&C}}xd-1CVFh()H?JlkTBe6u2Y$ zLogmX4U9t`3cCji6apLAbz99F-o@xxt z!$i8wOEKoxTh1iwPlWFGLuacaoDn4Bb4dl75KQ< zpU1o|n{v^p57C_A#E%TA4K-cRJ#Wf~c1VXA*dHNd9bo8T9^?fSR zKQ-xpSvvvhi@2O-?@R_afE-zqUGKy5A9_#kyqdE7BljDe1XtLq>sSg{Tux;&yYs)W zJ8SbNYakcf-b!A3nQ(0A1e$kKMOiPe&spJG%U4*Xpop>};3tMnJ`L`BVkP?Zy;g*5 z{6d!cBb;yW09EI45qN~u4BaZuzXpANHo`#z>%|qVJUY`pjT-=pUu;PkHR$gW+<0I> z%bQAP_2cWVbJl{hl8bHDCz027zkX{}FW|saljIgg0+&J$gW@>KmZ}J^+IBajbDz?G zgsAQ$FjL|}A&hcS0oThIsj6xg4di*vof(7>z5$AWT=K#xpzFv37az3FK@StRkoF4= zw(#*<%n(tB{I_a41<6G^{o=m2B@SP)|D={2HY<)4(i{R@pvStWc@ag9jwq+Nz)lEQ zhu`8&u-vw=%$CNY7`F~dKPm@Mia&!IvHiz!cBUgoJ&gG+SQ*=7JgvL-G{jrvB2qK? zl;C^#*l>jG*Mn0yr!9gCs?FG*?oQke(CCax#c}W1N*74~Ml7+pu%%s0q0szoCJ+vp zr;q$x3b{-V4(o4crUNSAqtR$@J+%Vv-odC8`UrUVmIu=60X4T6L!Gic?|k;rWS3H}hOhJa&@TZFjuFt}`Yw1HuV@7N*#F~2hfZtu z{6`^d7V|A=#IpLtF%!8{vm6HI-45KgZc(^-C=X#et}^Z3Akhgrz~Wiuj>upl;Zs$n z@84YUyr$-yHt=xxkdH*74E-~A$2Nt0dANZLI;K#fQm>P8-wjvFoFNa8aq~wR$@f4A zzup`$f2?ySE|E&Rd((-m#~B*IPYNH=A*|_l2w}5SdpLFbb@r^oNdS5yEe%#r3vKES zZDQT5F<6rzQJ3F5H*C;JAIxUg()qGgB3!qNFK5iU5Q)U#5UGMSsoe!|3U00lFys+& z&#Vh{4dtr+mcUiHJ-q@f{$U-VSbxnbgKp9))L^xGZGuX2fl8Z&P!%YF1Dc|`^fAQZ z&O-DcD45|~-1_9LA_$x;<8uppWa+^kx3qSRFnnY&VFX+JUH#>bJ4?QTq#1k{Rx0he ze_?ST!J-+{k})73N_N|Pf<;(ab;%|zIq%Ad<;;dx6;1rLAX^VuUWESbA;gFsTNxo{ zu+tqXCz&swYJzt`?MV4O?R*m(xlY~Em)uos9{l`);a7U<O$(oT}15bn6cIg!f$G%6e)(9D2u0Zx0m3Y92qP*W~(G|-S69!nRvoB`d| zqcs~@i2qFy?U*jDO!w;I7jJoU#ikAIQ#-{fVJQ28+hHp;Mjc5Z6{m%c`5 z{D;`2`w)ek(P7eT^IAN_WhJYvDzUJwf?B+zI|S9aQ$Mpl*L2wxKD7}b8ghXqwP@mC z+_)OPjFDpLG_GqnRYqTd@b*p!{N00?u}&2?_)aC#X+B^GsMt+^KMo6V00H5n>PFkwr#PY!n8T0h0(KkW8=K_vQ_rA|Uj1PgeGw77pl z2{(IZ;R}hy(zG`!9RJQ3V)8nSD&ZC940yE4?Y;buJp|=%H5XAROu}^=jXmI!*2Du` zcU9UCF?x^fB|^ic02=DNeE8vfj7ghSM8z!x9|gusk^*t6A5X4(1UQW)7A=k9exLfy zof!Mv`N~hDuPd&toJ6gh2UHx3G(dX{vJuOF7nc!zy(6oR?c9o_GgwCPRSV1WDu^Rl zDv5|ADJZG7)F1DArA)?_Ktx zO6ZcX7kG)`H{ws{CgYr1SQT%}CwY2tX&y|NGu>Is$@c(N8KkFXVa=8A?=$=0D zQL>i9>hb2N&lHT!$M7l@TB~_f@@2Fkpsk%wx%`goXZ=x-83gCZe@ctA^rBvOYJdeDsPA+%``A8( z({?Qyp1UmZ=@Qy3dA5!VAh(Uv7yfJ_u-Hb9Tt^fHVaR@skf-x(g@f;g`+P(jOJ0T_ z91k`S&7ZhuEU&d@_s)~5kdB-|Kw&Q`!ER7E6>8f3j zYBf0ox|IbWd7BQaRdwDX5>AWZsk53eQwg?0P*wk+F2IxP@Uw=cx8XaScKb5r&j1x8 zm7#y%myB;=ylk{Hakj``>;XlE8tM_?u zah_)G_afQ6>gzZ$kI0!~7K30G*_3s#)!s&8i6phvmSFEB_b98$+RNdDD2n;9*#A%@ zOgk&lwELCG5=bm&qe59>%FbNTc>O4$uS2oa@R}tr1vpbaiFW2tM|GXNzs&B}IL7j* z(&p6R`xwY6)pvX*!=G?iTy3{DsvN1b(!4nYvdBC6FWYwU7Cbg+p5{GW^JvWM(z)Z9 z*K%6M%}hwM+DLQ;sdTW}S|ButkGg|35_r^?+4j0p;npQ1R!?{Aw2vH0arQU8H@5{;4Q_MN^dKHiMB#XK^{ue7HQ z1pi^YtU~?wlTWnB-$69WrRMly4i+6z|6j`G4=C25vnVYehk!PGfoRw{zdD^J^g}|f zm~hDaKxIwrbyfz7j2tS%-PhLcrPI|Fq%~HA(~&UW>1rahZx5!1PE%`FTm`UkaB1xE z=DvI1s%e5Rha>ydbS!5c@zu-Wy1fuEEv6ef9bLX3b?Mtiu8_ou`lBb(*AA3)0-iGp1{6nJB=l2%FWg!+R{nq*X@<$@aciOSa39)>WB+=SF2I(mJlMKeH4GBBP zXt>!=Hv6ki8f_{epiY#N#o=e3zzwwGQ%-LYm~arne^eov1efA74$mH>`#%m8edkZM z#|=<5##gMGOJ}$p11Y{QWQTdYVt-@W(pJY|h)Ohk75In!#7rzf_=}iauANxrJqme} zC~D&o8L458@NREfXt;fyGLf_iiJ6~Ie4F8~!;K|MCD4?U=eq2E%>6e+7jQ)8a&R~g$g1!O`k5N-u z^yUL;YXqaFPV_$LCVWulcwJeu@YXI9BZ=(>5hy>aq(n+RGp7`1V*1q7J(U)u*PK|+ z>>J)oqT5pKs(f~M)mJ9|d{v3(Fc{kfs=Rd(Ll zN+(a?ZT9a=uJ>8}2Vi~j^C^@2GUzfj5oJ@fn$n6Ds(u12XX=a9v{&S~T@?TZA{MN@ zTW+QSCjLr89I%wlL;6IRV2#?gC^mDTjopd9_ZPAFl!|Zyeo&u|=}(_`VgRO_AQ>l! zVHe+$Tpq&DSSM`$d7L!Aod8DJQGTSU7j(TvuCLDLK+g2o~xi{96Q z`JP958I;#$sq*9VO|+IoxPI{oddk(`v3f35+Oa8hhWb+>yByT;BJs5xzd_~B#|VW! z=v_qjrpyIzwN&mBqTrLM z0Rl%h|@=4P1h;cQ78VCTSs+Vj)rbhO8KXe34mJWTA~I1Cq^(ze97l- zYkr}}0+=e<)fOj%lkhnHBAXxFT{fAI@K)3`d6g1BtFqqcq(XSfk1yz$c*)>-gq2-i zQk&Z1(Iff@{Q{FeM-d_DZC4@|GpTHk1U3X7KCkTydcsx-B$=^5H zmaeJ2srS5EaBku?ygj_5=G52o(c8 z(GYj;+NmzG1xzqIR_!-9C(n(Ke516e(mY}jf>og2RMMOM$R3L#olgKCAQz<29>Gae z3FL{mHbOvT7p8*f)myOH`UN<5F!NC;HSv?U%OM;%A&2jU)pYA+7Pf2rruaD|3nZw&DN#v5_?&RQ=6{&AaN)#}{B-L0 zt7sJV+b#@EC>powF_YVzngll9{&=%-^KM9LUi&r2VaUAw$uRG=invGl!wb|N31Wci z8mP17$xCkPh zHOL#SITKAIrqtOrRriYB+FW()AFscGkXJM z-P{ey!G{`|(FG%ayVbwsNx0VF384`FCNEMjTR85slt)Y24a08S@=&hH#=#LJs{*~} z;OZ#oIuah$AbTyrIL;V%y>9(|_xs0M!Wo>-r#p{~`7PhjVK9d?%jirPATK)-C0&RK97(R#D`QB#tgy zR8}3P-=E=wnDAOY<9{SocPZ1?WnTFgdJohQ((n6xZTE=rJ~FEr9tdy#5eQm;Eh+5y zOJ^&!cYEk6Ms3zWIMqoKz5(g$9r{cePQFobPBgzDm;~^74nb@@3bPjH-cm8xE5cc< z3JH3xdxTkBx2ERj_6JoBZKD12%Rc{N(=+S@rk*}rJtu2@dH<&yQBQZsfyy=`^tcIM zCAN#=y$e;rxB3O$Uo59TEa}9;5F$s0;VCKja1{2tI4N?SRbPcLY%(YxB|Gm);&s2a zcY?OOcYos6&Vfcu_miRX0A2$csg4T~jdi`s_C*hxZS7%7;BOg|PDMhfkEet}LvN!H z;0`4eli!ayL7gx_EjC;y5(lNyG7gzfVNz01C;_3w2}z%`J$Bi>_78H%Rgo^e;G=?X9WZ9Fc@i8&q4{jpH zkT$iUubIa^7R<}3quF3ZgU01oCRP!s4~wR448V6##|!EWkMDRVfI0E;Zb<8mC#gs) zy}`XGUpe(~83|Hl;F+1r1BAr>!!yt8&~!a7?h`s{(L7S${h=lpp-olr$qrSowX>A? z;ES4U`G-GnTg_LIg1bO`P0}%{Ee|si0fJr%8%k ztCq_*26rVni55KkNq<4lj5$ld zTXeGteumhfsWW#HNPOIQ5}I2WV#;Sq!6{R2VHD8ubUW*nj{&CNcsvrVW(M-=sAXj# zKuI~oQNC>CGN$J(YO@awfWv3Q{U&oUF!!Y#n(nlXp9<26m48H{q63M zO5$uGnqrx#uqvA%;@-OIHhPw4EfFbpEd5DtDsKu ze@dmjFO#w!B3@T+++JJr^h292>mH&hZ@F90?6UXeR&G&pP7%*A{b|t6g}3vT*3OcsMVuP_#u^okhV`V4 z0?_!t3=i3br!-^3O`>Kc=Y1R+wTN%M!<7wa94Y&s4S&<7qVU8Nh2?O z7HLfI;4QV}((!N{z(8X5tPDi4F6Hywgk!hv71Z{cm2wO&4jmqtcVsfRSD1dM3EHCO zc|x&RQ$bkLo&S9*_VMy(2nwjrl{8DJcbxaXMt>igTj&?0+drD`?ApYN7>j?b#5^nOpAsy767co?9_%58dE=0JAq?y*VC`Q+ax1Y1nad#d{ zIaJY0qSJq{C@lZvX%hy0uNub|{eLY0!yNAO7A>{e9)r z&?S0h*Vc?E*-vWdLGR|t?Y}bEEcq>n);w^`NKo@0o9z%h_?v{TCZcjb&hod5biuV7 z&kZ#(Z>0-YR8GX_qtq%PX_NjR`j?0m=KPbZLPob0^Fy{#Z)aS>VozyzcKY-C8EvGn zZa;=>{V*LwTs10-&tpew((GCsBA*QW#wh7>`HW3^i5K8LJKW_(xkGv1AIHpqP@r}! zSD;1)rBbCkx%)E~06bs1*|MtKicQ7g5$(^SWe5`&V2LnhMmmr}ayy1u_D2&~aWcn- z-EMG%kSbB!;gHH}Tx|}oxk{Q$GNMb5L|qThv;%60tI>j1X^= z{7S2TQYXc%z+xQxa4Lu?-%9I=smghN*f&U3yx^1{ljbIl=66NCHJv6JY%6GvBvk%-+&O`_L#Ohy3U*-aB^opN z$zIXnM^BvX7aaaw^QT|6DI`q$wdI&k#Nj)3+b1n4eg(OVy;TP??u zmcVO22Ixpv)-3M>5zm;Zhg%Lm;&EY!7)T zBDl(wE0jv{7`|&ZNPjB6SUIHL`7vxvf2tN{#s|twfgj-4^FLy$PE$Lz&Pj%MhmC}s zmx}5zTf50?zpzX3@?*#wLZ(Cut-&)JUwO#1`WFTfIx5NIB>hvg!mAOtKcL)6HQ`8W z?7yd8Tt1WH-3hoc+&tGJU^dTbhlo)V9MCoaoVbb(@cYV-th-&V@bz{+uZ%Lr@D*j^ z)EI6J8Vo*1TTX1utydW{{3%~WK4`j0iaI6Xgf2J?yS-(#gsN4omH)kNQ6G4Ib;pS^2^2?cML)nbIF#t2(Nq;BCXqF0EO6O%ZmWn!GP?fH{zcD za!YTBjJ0)BCewyS>Qi((z*KO~x#~Qu-V<1Lp}e#1-fluY*g3P*lN7TJ8@x3?vufl| zj@`#^jF(kgEF&F#JzNKcPe@b$X0`h9=kTNfzvpiZynvTrqUZDD8~ZS1Q3LR?Mg-_L z8-u#Hfm}TQ>A1hVF9WN2)coPOoQf$c+uID-c$jyY0e^F{Yg`7(sBsNK%ca~d=Wr9? zcr!RiDmYIcy+xorH;)$DQ!?6OloROP0?8WybKNxNJm+=26rVkR34hi&89!I_*QmNs zZL;nCw`#A5+4KR(#$`+kJnmOc(gTJPWpU>5QzS;m^C7O9qVeCLkje+)9z8JDY>-`D z;=FrzhD?qrf!TN@@(8c>IC63=aY$5P0j$}cY><7C|C;w9@5@(Zw2uy13bUaM53qN{ zXV_6xqQ??+T)lNCVI7E|cV8nKM&jBQIoh(-!z%lDR^$#9=XfQ~=sv8tI!P)tl}L*P zZP|ay^p_IykOKL&o4oCqdO9sK17XJHBlZVUzM8fI4r$rYoKJjdR!}>Yc`&fcj=?{# zAlN*~1wQG8zdw9Gke)%8LG187LlR#$FWh{9W#b*a7h-VL6L@I8+u|)LeF%mr@|F~9 z51r?Z&M=r*Z|&8X+5a~1=hB-cx2#2vDQ~iy1kMH#+CsT9O_$?QinCfEs%$9=Qh9>J zjjBQ~eM6<@W=lW#nbSc6iJ<p^T&K%#viElV;N1e{IqIVES1dT$na+R56 zsE7w9HW18fGiLvte);q0%rKgYE!dIP0)RV_;VBQe!KO=NrwL@v1^d5j3rbPL?HnU} z#|?><#L3-e#aOcZ#80RD9Kzp4hU+~WzT0LGBv)8M(Yvp6D%+W1L2wZ?2T&o`r6Dav z0_5n2m^P9I$$Oz`VX%QiT?#;Fyvn>_r;gw{LB#eR_js8@)7Rb~v7%$FqXe+DXfV`_1Uz`1S-CmXKj&$m+%lzh0g_izsf}4%mi36i}`?R;XmVq3~SDD?{+QP+dZj^QUz+#w+g6#2{Ay z!&U8-T%l$9AS3QC3m=o+aCid7pWgqNoX%)wj)3F^95MTm;tp zj!AKUG1GA-xs6mIE%67IAfIH8kDX~T z(MIL%rwQRj)xccYx~hw0s8`%WBmVM7r?8%@ny%)Z-7Ej}+#YzCvZkF$R;MZ!gjIO~ z?fy^$!ip-b=$v#WHP!1qdcq~gdSkcU1Ud)mNq_s)vEVlA(T2WhBO#fRYtXT9YC>jT zgsNhvGzqg8sgU~y+L$=8B)pKh4x6sGZf$$ja}xO%?>jI5$cPXXWZ`YfJsagvl=a`5 zek!!XDeK@kU4XyRdz9SXYVmP6dn<$G6-J>5$|w>9{5z})Jue&hG+9%zlmj@%+0udn z*6Eb9v8+>VpXF681RLF=4WBW*vqK|@1f5DhUy{cnU>fO0Sq!~gKKuo`WqfwmY4W+j zC=u#i!6Pil7V6avN3iCI%nInHS-gKkxVgYUJ1#Uq*7O|?lmGnR@&#w+ieCdf3QJ}` zB>hC@Qq2VK0Ko{#(c zrh})(BsL#v^{}~Pa=$3l;U-^qN@Q*BPq}86pbA?6aI6x^=Jmn&&{c-c@Q z!&qgIom{_a=A)ma5W3tezo##a!4+3DM}?J5pOM;i>sdg-z`_@vpl+0}s9Lvou?9*c zR4J9IfoPdOe_%+K;}TRtAnQ$5t*zoj2x+E>?yKgu%|~zb&}} zROBs`AX)3~GR?5xUxrrIvJszzIWMvOKZPdtt9JfMs=4FL&Hs5iu?;Gi2XH$vCk4e~*Q<9hQM&G$ znTkh@XpFU9R40q!)Z8S~HBY71p}1+@{s z@;LTYph5lkc+rfwSU49Vv#vOiR7WR3*-(tZTQFN0X#2Sp894U_4w7TI8a=k|_9k-T zIi>D;5gKh`n0N)#`N8PvgFuWgyHusQ>Z0cFK+{3Vx>v@kZDt=x__mYu0DC4&-U~AS zQ$8m;-kWV`sgsyzl&8t^8uE&>DuXPC8+0kk8q$`e^kCOWd$4(7U+YPGc3`L91ef@K z8#rEqM^s-bx9(F6Tq2eNh}?l#>a z^$jwxcBA#!Q4<00adIzJi&b)Zk~n!!ix{o%tY_ETfsJ)vApnSdV70{JkVJdjyowP0 zxeobR?`#PT;-@3gX;{_zzDre9dxQQMxRLaX2#dU0{<;kliX_UN%PbJ1==ARse9J(`eDWO((w@D}u%uE@uJRJgbix?Dt zPWX`ZV^Z0wvcH*-T3X&o|J%`4lk#Ot7Dn+nc_y+3&7TK&Pk)^~Ku<;BG}? z6cnXZN6<4Nc-hpMN+j^tx!e&u=bx#QC$Y)olqF$rhKHaRaH2ote`;p>Iia;Qk%nqq z7`%*pn>&Q6=X`C;K}>3=s$wJzD37wR?3`F!1uRMdp3MlJOw(;lE1OI!XN=!jbYZxQ zONB6C;$Kk9<#MwzBUo^VeRA3XFW$FCWoA!)w~8xMi7isqnGWk*5$R8Zyd?6(<$uia zh>`C)f-3#JxR#KfuYYF=UjifDni!EaonET94!M3InTJ$9MO`+vBPbmH$$Xg}|mC zLWUr6=a_gDUACY>d#4)}=|cy_ZEg^eZOa6jy`X*xa`dtbJ>7A3;xOt@%tF8fGVT+3 zO%V={`;~_R@K5gvE6>Hj-!tifK2Q|19kaMIj(Co8?`chz3AVoUFX#S3qb0#GB8PY) zo5q_Nr7rz)pAXXgufyjNqD1!))4kdTE*Cl`8=|Li-m;asdgp>jM!Vj%ZSu#43)wu$ z3#+Xi+!mEx>dz(9Yi7v$ql%X#8^dx;+)r0&TwuYmFZ4s^rEjA%_}jWIq_(Y-s6Ql= zmUh4M4OaA=qmU&T`7WaQf7*yh>Bic)zu9*1=xJLJ-mevwRoyv;FEcP!2I>9TblX;0 z7<}|gWoMj%oqOyc%6(ib%5p&#_{e&~*4yB^+gqSYYrOxt7q_s*K@ekOYTN>@5){9x zOms67wxE{Ds`XH0oB~pxanb82dM56w3by^eV}||0bKPePBfn~@P$j=j7PEMu*k+du zTd>^RSydeDEL*~~ZqMU=;z{VY`!f>H5Kf8zgKpWSkFIUvy^*AJhcm!iP@029y2+2T z9(`ttge${Iy35JKMAEhOMY8=JTneY1BGGL^p&>`G$W!`f=nOJO?a!4-ffz>QnJALM zR3pmIN-up(5#6A>aQy<=>4%#*Zuo(N<4DI5s`yZCtV78g-4w&&n|Kg3DNYGy|E!TX zm>#%N8H|;X`+|3R^GPdfIsf>o3kCk=u z_wlsDg}F{{E!>|c{rM{Joxt&nJm7SrIh#-@lwx5r8mFMsr9lR_&^6>S#6t*-;{LDj=0+0y%P;nCV5hk~5AyyQw>Dv~U{EHuALBh@$ezvVyP?aJxRqf$DjL1|*Tk<9u2y74o$Z)xT7yzL|5AOH zAZw1Q+KM@2j;SvzrV3``3*fdNq1ok3bQT;X#yU=bs*Ajj8z_~oo{{@C*C_TPh&}4# z7WaSAig$EOdguo~U7o?7|;?F>JlGr@Ro1ZgqL@zVeo%i*bF@qB~dS3a#sNV_^(R7IvkOlXu-1x@NeHVaiPP4?N1OGZ!zi1P)nX zX!JzIaNSe8=Ht4iL-^UW$XE|0{JFno7v;ObOVLQwW-bcE0#-sgr;!c*Orw|{>7_6y*C`_T)fqx$iP(S)K2l;K(@965e# zR6r6#c4FKR94{1<4(M{aPI|ub?^4mwcmfl&Pt*YaLxZ+@L7fMM(Z&%}7AqhVAu!+i7eB|U;3Y_kxWL@29O<{xcq4N9FcWXP(eR9#Jb&0mVy zL!`Wv^775-GBIg@(b|u7r5;v251EHe}XGe>nJF8W<&-57&0r}xZKqDCFcjLu~B%((Q+s1nYn~ri}oIK5Y4LApKqVCWC8-?2UWEaj>O}8Y5lNdxeLtd5g|BEfGCAO_Q3? zt|0&LD74`WFcQaG-Gn6C(zLIUYZG=(YBwkR{lTsZ(It zc^Dd%0hc}|*z||K40lk$Pu#WrOC-&Y^WDAI%pd=ozo5R{mtf)&SfU+21~-m2(ueMC zOXHXSkYa9y*muGVoI?pn<}@DUoB+8??h06m{tby(t+G4Wv7b#ykw?-Fr0wd$lQM#Z zJ6s}Kj}Aw&0;&SVTW+s4s623O`^-oUfAj|Yv^XQc&-ld<9`{YmNPyVkuueaw^$njC z{xeJ;$s4%DBb*+-je^O6e0y~Bg-iodQ9^!jQ?Z8NJiiNt6fTGG*~k_w5{|4UtQjJ% zvAX4EU;0atcu1M&eejg7T@Fed-+!(l4xV@Fli#ye^8EDzQiggt=)7}+5g`W9n5Z!z zW?2Pz`NE^xn^u23xAaD#cHi8kcpPq&+u}g*MjBI|kqq!eKz1I9@|y}-VJTGk+B`mf z%f*ifbBu}GpYRSPI10Hlk{~Mrrfeka9Y7R3MBSU#O|{{BGh`J0MfoMp#IyNDzpl8G z=_!Q(ab@(5Tdx%|_;j3~O%AAJ>xDajdBW8vZ79S7T&9l(EM7F(H3JvBs>c|06n$gd z^?NC7A1=^hth)ia9H{j%QpyMpt=t^Jg7h_)&CP-+<{20{oo{(Di{kI~Z z0cQZp52=~s++v9~5b{M1%n`(x$5G~IklN&{6xouiyL81M{P z^z2XBh#Eq@zj2tEMcTrpytp|YYKK1It@DDEn`dmVoWE-%M$G`X%??=7UEm)582G)* z6)fQzm9zvnCdBnFGeKmbuTdAI@f1w)z=}ujjr0Be67Zkkq2CqPgw*etJ#6!(2)FyB z;BC3iU~z1?@P?;6qww|bjNyw0FId1-h#c83{`j&fhx_h{8(u!-kq)i?NrLSVc_@V| zTeqptgFzjW(oVeb!F$Dog`1+?3sVDD@|I!Az~J<+3m@9`lKl7Li-~`uJ;#!LN!jsQ zrNxhsLA}x*?D%b_WvOevS}AFRvbl~3=muNTUmvy!n1QMiyi09$OQc#G6ULXWe zkCYC1)}ZlNEkv=f=yHOyv0i~^uxgy<^#yZ+D)qh=Ca%C#9vQPnPf zOYeunraENF#0MCR?XUf?Xv;jbJU*j|`~{LE0Lx<9hD}})b$`Rw=^2Q&qrVhCJ0uau zmAwwDwIEKBKN4+QGvo42RAc@u^d3zyMLnW&=t=QUE;4(q zChk0#)ed!IpS2iLux zS=qv0Q0UX?RgQYJSO`<_8S(Y%wDBc^sD@YqQBv$f-U56R7pmp&GwFGLN!!DzOY(wP z$0JZf}T@!CpzX$isH8M7&Rg4kPYu!u z^#-uts{n>64Vb|bwJ$rM^FW*&p&$mya2b$IM{_zmtA*mts=OsUHBg|dT70A|KpvV* zaRalv5bZ7gHFw_)UpIkLO?gyfvkV zT29U(uzOQKQ+-n+jxZadpQ#U^o{L;DFRyNXw3`2;=_8|tWZdh7o)T2XB0>mQ`?xL?OViN3Bbay3IbH}V?P+8-T%H+WBVr9F*Wug*J zzCs6bv+mPuVM=WNbFFJ?jjrS^y-Ae|2CB9O7U4hdpFeHh?Cy|RTF))}vB1qE$=p%* z(VG0PpmfCeRkl^HP$(t#3Z@K^2D$PQRx!c2xE(B|8YRfLYsa?>$LFGKX{J#q?jsHI7J)ffh z2LNDyv=&YJI1gC5;NVTCk9}q6mRF@AXtQ4om|%N%alMoy?Cc%vJ^S#_3%xT0#=Xsq z$M=d;G*q0>ggTJ=fscWg+4DaFucdjc8T_TJed%GX03O4%B#|9*4YvfY??_(LZbI`C zq7A=&-B36o8FHbc`nljDR-J{CsfdLg1wY)U|5-9mpZQ^6J(0qsvm55>h9FbzE8moP z`JLuPrS=}T>F!TA zPWWP>7|qunRuqM`Ya?my4iE#?94nXMx05`ITE zJMs|6GM$S8?`^2EtUuIy;q#DvX18{TGDK+rR@nz>CY_})4N4Hun|`qN+!4dTu4;WO z>blJixk%|9%WP(Nw{^&LFdZPqpZPiG!72}edV=9mt#V2ru)Vt)vj?k`g&9HIra0NL zpOXTNaP{micy7^^_l5Kk@mANF=%Cl_adokh_36iXM7qxyKd<@_uFEJe`~8t)oQTP> zZC3oa=s%w`miLqB`D;qZ6Z$~P{59_vi>16j?5sHQxAdy2Myki7-JVx*B*eVCtl$+v z$Ehvj+H=Pu9_ldPQh7^6MAnRxNTg{pmFt#SK4|5S^Z0Bpgd|D9CHRwBZU37~yK8i* z2nR>nt(wHHixfeGxR~pZyM~YqbdzC;BPnt2sjd@RZ-T&H`h9S<{t0WxWKj5Wm_I6Q zbR#q+I26{VqSt%>$Mk%nPBp;|9qn?3z{I=WuJM@U7Wmfd!W{f&GjWXh!c}|k&j}Bb zdmLx-Q5`8cg%viO9p#7wLZm;!0k*aqXx-!$xiwD$K=U=dVzaPk7p&yL=HK&vR8`MS z6e7rLLYK9nHFc)T3{k9LW)N?Sc#Ra!?J0@FQZgfF*j3Gt$sPuj}h^wmsbY^*A zEghPQ5a>;Hj6LURa@cPt@A+d&9>Pf9BPTlf$0 zA-R~owC8B`H;>~Tgr{Ui7j`zG8KL>*>5x?~uNTZ*pgfqv7iRA%MK!%wKXi-zwUA|~ z9^;VXNXz{b`iAm5Ge<_5kJL3dLqujEz@S`3=Ewccmh3?l!&;Vc(DQt8S3|_@e8S{{ z3ed4s^*G|DCZ}+GMisJ|eL>%-y{C*@c8u@G1ja*k<_sg4*%X8ITVl>nc{Z$4+U_Pl ze(QVArvPTiQZIuj9ph(sMY$}8>Nf@L<0Dg19+lT5Co@;wc)wk_?F=Fgb^nRv9Ru6$KCfA#A zUrb|Cg>ULj*d6Jf4c%T;P}46ZLI)}#eWgg7TY746l1}P2xa^+076C7HN2h2@$9nuh z7Cm0w8P^)Wh#1Tc`bvuJ3A=xFOd;p1`|?du>B6<5!_e@lkb0a;^0i`I5Z-CdqLo3( zy?-5EeZm6G(>KJRMt_Yi~8E zM|W9QHtFe*kBC)U&}$0W%KbCnM!Pm|Az3*cbSuriJsZtW+o=5!LQ$}$ zsO+a=u>~{l;K=jtMT0BvUstlVVDl2^nm*{0v58B+!a4cg!lh0l(Ldxzn;uP;EUTdYWn#p-{Kdh?f>H&p;RUy))`r1|o88-gJY(rii$xQ<9nA97M&omtpC{mc6JdaBAEdGnOsd?2 zGC>U?XcjB;=%YCcge%wj*)4cAEkqk|Y6qJaFmvjcc#-71siSiO5(v!T?iuK6KBI=1sYhnWOLKXEzruS3JeqIfEDy@k*WL2A$D* zYWx!i(gL!}WM82E*US>Ob+}PU<@Mx-Wqnr29f`$KWg$iGNWw*OEF5CZ1`?%ft&xflQM>xC!?@2zb7N%D}Qe{c?QIcWYdF~n+~j@q)jRan&ES75 zO^U!(#YI7Jf1%>og>Yq=$+NNFwBm{^_yIdwvi268re2$Ioz~H$d2RnBS?!5#gXe^e z#bet_`sEj7v*1l#0HOMi;i0r?!rFyvI?LCb&=$Wau^RAe;hC=qv2|GAAms{P1}43- zcx!CfQ$o!Gi{QfLZ45Of+AJ78S)7R!j*t*;wom6k(*l0cYdueeJ~@zUc3ccDEx6HX zK2x!2FCN1dSn=T|G<*T3K-jtq4kR2BZPbKSvs-65j~K@u8Do>Yfq1K8XKVAL z*wJAlmFRquse6si=O*`>P`n*^MEgz#Kah}%iuLvTKPJO^*3z%OSaHt7lxm)VB zW%-b+6#YPjQ#Zl`USs@EkPAVU_{+5R>P3hT;7tnf!0}N2_WVc-`lnuaOnvXL+7OyF z{aR3U?@2HJ`?tCGVVo3z#&pqIv`^vk`38m|OEqDt-BL@xLIX#N?fRyfQzr-W{vu+p zoviHSQCNdZYWh>TMGsIC5qx6jxMaFSmUbfpo3Hq##y-df1po|qX52v8WQWYnwY+RP zB_tuJhJw=Tdh(Sjs8}uD)PraMsi6nf+?sYLPM|q+iL20l>Ll8FRC*Vo5v@>enM8WIWrW$ zN*ZqIG)z^5+!Qy<6@$B(h6HCkXog>s82>*PP=et6d$xfyE|-^4Fus6`k3K8zui*0*E{-K{5rJ6&P?nQ|~1%`{F7JsWdBnn(MP?^TKy# z^_Rw?*B(yPhh=M;Z$3Bu?lCUx{^?Wi>`Rx1O`H^?NC}BHy~?03^TNN`&L_Ii4QtUj z01yyfvMUPppIZKrFZB;xAw%M+D zzFl3tYVf;gyy?*e>qW3L&JQ67Mm-()Aa}odJ${ECfQdEA-dA7;Vcv(&er07tS=O9h|G}HP;ye0L)(avczqfE zLsp<*6j3`EDIYxPear@7KEy#CX7!ceQA~V-M3UhRk2m-K4V7VMeoPUM5QvQxw38rh z22&huhu-=c{qHCbSPUR2wksmE4!}Mo#i&m_%uRW4ZKGKYo=$;oMeIwx6!DM%4(ALt z*`jCuwr3pFU}}0_KCxx4qi1&D%<9=ODGsNgaHM#S>vxSK;5IE#eID$06*#hfglHdi zAm(2StzM!sxw5;q^){=eYgJG^YxTGAo}wVHgZLc;I!SMd5{&mpE*nDnO5xHF5M%){ zOdqM0%?Q-JqVNxr3Tj^3{surHJ(_r0_-IDN@h&E6`_oU&P#Ad#y&|dK3VqSnAY{t9 zi2Hg_ei_heK)kzto9g}3DCc3OzO57K`O-J1|^1eq$wna+ELL; zz9x5!O7Y-vnS#;Z!)?seS;r;PXZ7>YUT?b<;vC{S*cK)1>a7G{I-1cFn9C&f9<}B7 zZC5HhofFSSTf|%_^<1wxeS*~MpF`>fA&b0={1|>t#FLG8b)>+sk+{Gs@6O~Fq5 zRlW3y@jc<)j3>x)omC72+v}{Rh=8x_p@R=_W(6NjjH|-+OykaQ;)}j8aS3+id!q{mf{Lg$8`{U(YMa7n=4ASJ zijrtrq8(%6oiP&E5`u;o^}Qu-%eC^GhCsG+ju=SK99*(pczqNJVh2XQKIKC(>WK(Y z2Ddk-$A;C0nbj+@*gY5M$I!8>M^ijS_2xWOs!qBymAJw*v(3_wp7|!O4)UpCoeSIV zIBx?1wLH{f4mXYIqiUjEms{T)RN=an@5A%@I#ep4PqtwWH36i4p`1`b@=%flE+gpD zeO|Q3w!z=2JhT_k%c8-^NuX>73pLqXOb35%>@B!8?m53epaZz8oz;V{0WT<~9D$hx zXjA0_GeEh5v~GhZFsc)Tm`kQdHXE} zX_KOaKJd$-X)ytCLp0@%Re10bdGFI#nCjKBXt}=95ots3459{|8;Uxey_cdrlurwr zR1lmU&*tgyXQBP(b~11)vR`P-{f5-C0Ng=*=HCbBKe6Roz(l>aUy=8)1tET$#dBb7 zjaK-(V4PeSDYl^{$aG)OAYID|)6k<)74N(F0rmde7yh~+k{%!)y-yKmqD(cFH>cJh{+QKgrh-=6(E(x&2(}Ep4 zsKKzYxJ0=;TR0q|!#G^-AxzU$b-e8~3rP$WfyOYk(h0Y{S9aXjA} z@cR69ZkYXDaFn>z{^_CCbj)u^8LzV&$3aFWkqZuDtH_P*(tLhXRVr~(zAdzQ$9F9c z=W-h%7#86X`mmFX?ta3=pQ`E@e7cBR3G|J04P^7@M;#Z507+^u z$e_4H=1n1&1J5nD;b2n=$H}rgxV}>lLpRX_Dp3fsfOA)Zx{}ZMM@^TE*uDCvCwBgl*SV`A{CkeEwAz~Jr%Rk^hvkOY%CaImJj&r5%YtPp%}Ox{t&N`w2BA;Rd{7q8`E#IO9J67L7Plb7QjYZ z%^?BR-@796{=vO@qd+|-?5_>=V<zn^8Ny|83>ty`#e0+n3`Yr`n-n; z918JD!neUj7RyBM@pIXWe%XV64;2g1B89(#I02RIb%-V%x*kC#db3KVTXs$v&Prn3 zag))MQd#&+*z9}LlHtzH6jiMLV?2&gS!*1ujIP(*3b?tLBtDz5__K3^sAz-*|JX!% zFHOU~7uIl3i}K=Mu#fq{7GE0+G9 zcMIDh(=j{v=JNGA2blMnPYFJ|Y{w@J$VJusW<60cnkhS%N|0lMrzT@vdLTV!<-Pbu!YvVR8B%Kl_@kq;$4H<_3!3kCBr4eJjfVtTWzP3IDzg#;bGNr$mxvPm0 zJE;rh!%$O=);AAi8*Z_`5pphw<^8?I%dHl)dI40b z(d>8YWR0NhyI!k`(U|yKsm;wJ{SZ#9HqCU6c*?whFP+~Rf$fQqk$?*m7#bv9s)bj> zXNe?nlGk(_(Q4<2@G&^3AK-QVvW>A*fC>i|48Yg8UUc_cL%U4eT~jxemc}oA9VMK77KjBGRq(U@a^iV@y(Xhj z=AHG9I%@HLcDdL~(s+$P!qzpzhT*Y(vACRgr3dib+-iXQ41fzMHVRhf4k=qfN(!f! zDLNy4FVGy0oJ%pZ^CKihlLJ@#lC*0>$Ar+n;qCK;JsI)ye{{U((Q|$$#`WLz≶1 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

a&RA|{>vspp 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 MlTZ+^{Aw8ZKlJOuh5!Hn diff --git a/website/public/apple-touch-icon.png b/website/public/apple-touch-icon.png index c171ce48479aa1b1bee546db6c61ba540be094f8..211e35de2f99e8b55119ec7bffa79de01bc8aa40 100644 GIT binary patch literal 22270 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 Yc!|W47K!}p|KUO9KPpRCNty=z59y$sMF0Q* diff --git a/website/public/favicon-16x16.png b/website/public/favicon-16x16.png index 6c14fa3254b5f22e7c19355fd5d2552610d47c91..1b971dc18c8a686621f8d13889c18098e8726327 100644 GIT binary patch delta 667 zcmV;M0%ZM$1G5E?B!3BTNLh0L0000000031gn1Wp00001b5ch_0olnce*gdgAY({U zO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy1ZP1_K>z@;j|==^ z1poj797#k$RCt_ylTB|DVHAbW%>ByFUrk;eG3 zsaT0|W3_SV+Ko%K|G>ENBgX$=VvKIwxN&Lx2{y;fFry>3y~$n7$$Ovoz4x90_74-l zMF97_TmxVoaQ2_j06Yfp0{|!TCx9ma5(gDe1Nb;T9HNq{Gg0UIor590%2W`FGw&H6UY#(g(4wYxO!TLdH@@(DmJ2zcl}Rm*kR*mz5`Hl${rrkXiP zy|Bv0;0^VBH`vl6zgPzF%@M_LilwDz%*@@G?4Rk%b(Xukl)`CW`wl>L0PR1wPn~lC zfc75%P+44hNZjnX%N2~`I_K72(wraBDExhHVt;y>roF{#{{>U!^&nv209Jj$ zs^4a%_nh(EGA(Wm z3X;mq>=nvJgPPf4s&tvMUh)Ag2Ot9YEE^RjXZqB#XBbPjsF{lt#E1*XRUDV%h)_xoAdan>u>4)^jy%XT1Ob002ovPDHLkV1ijv BEMouw delta 363 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 diff --git a/website/public/favicon.ico b/website/public/favicon.ico index e8e1c26b02efef5c96d1a88266a4de95943c5b8c..6324292a26a92b9c99ecd1b13b5a04dcd1076a16 100644 GIT binary patch literal 9608 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 J2L{>${{xu=h#>#~ literal 15406 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�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%0Ax1Jqtd^98kT7;JTY#Gut(Knvd^JXpj=@cQx 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 literal 91271 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 H-`)QM?;rZo diff --git a/website/public/images/spotube-logo.svg b/website/public/images/spotube-logo.svg deleted file mode 100644 index 5cd88f8e..00000000 --- a/website/public/images/spotube-logo.svg +++ /dev/null @@ -1,349 +0,0 @@ - - From ca6924f5a99fe10597614e3b775e4293a1d68ced Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 18 Sep 2025 23:28:56 +0600 Subject: [PATCH 05/47] feat: show plugin source and set the only plugin as default if no plugin is there --- .../dialogs/link_open_permission_dialog.dart | 69 ++++ lib/components/markdown/markdown.dart | 60 +--- lib/l10n/app_en.arb | 3 +- lib/l10n/generated/app_localizations.dart | 6 + lib/l10n/generated/app_localizations_ar.dart | 3 + lib/l10n/generated/app_localizations_bn.dart | 3 + lib/l10n/generated/app_localizations_ca.dart | 3 + lib/l10n/generated/app_localizations_cs.dart | 3 + lib/l10n/generated/app_localizations_de.dart | 3 + lib/l10n/generated/app_localizations_en.dart | 3 + lib/l10n/generated/app_localizations_es.dart | 3 + lib/l10n/generated/app_localizations_eu.dart | 3 + lib/l10n/generated/app_localizations_fa.dart | 3 + lib/l10n/generated/app_localizations_fi.dart | 3 + lib/l10n/generated/app_localizations_fr.dart | 3 + lib/l10n/generated/app_localizations_hi.dart | 3 + lib/l10n/generated/app_localizations_id.dart | 3 + lib/l10n/generated/app_localizations_it.dart | 3 + lib/l10n/generated/app_localizations_ja.dart | 3 + lib/l10n/generated/app_localizations_ka.dart | 3 + lib/l10n/generated/app_localizations_ko.dart | 3 + lib/l10n/generated/app_localizations_ne.dart | 3 + lib/l10n/generated/app_localizations_nl.dart | 3 + lib/l10n/generated/app_localizations_pl.dart | 3 + lib/l10n/generated/app_localizations_pt.dart | 3 + lib/l10n/generated/app_localizations_ru.dart | 3 + lib/l10n/generated/app_localizations_ta.dart | 3 + lib/l10n/generated/app_localizations_th.dart | 3 + lib/l10n/generated/app_localizations_tl.dart | 3 + lib/l10n/generated/app_localizations_tr.dart | 3 + lib/l10n/generated/app_localizations_uk.dart | 3 + lib/l10n/generated/app_localizations_vi.dart | 3 + lib/l10n/generated/app_localizations_zh.dart | 3 + .../metadata_plugins/plugin_repository.dart | 300 ++++++++++-------- .../metadata_plugin_provider.dart | 13 + pubspec.lock | 2 +- pubspec.yaml | 1 + untranslated_messages.json | 115 ++++++- 38 files changed, 464 insertions(+), 192 deletions(-) create mode 100644 lib/components/dialogs/link_open_permission_dialog.dart diff --git a/lib/components/dialogs/link_open_permission_dialog.dart b/lib/components/dialogs/link_open_permission_dialog.dart new file mode 100644 index 00000000..a7212d0a --- /dev/null +++ b/lib/components/dialogs/link_open_permission_dialog.dart @@ -0,0 +1,69 @@ +import 'package:flutter/services.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class LinkOpenPermissionDialog extends StatelessWidget { + final String? href; + const LinkOpenPermissionDialog({super.key, this.href}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 450), + child: AlertDialog( + title: Row( + spacing: 8, + children: [ + const Icon(SpotubeIcons.warning), + Text(context.l10n.open_link_in_browser), + ], + ), + content: Text.rich( + TextSpan( + children: [ + TextSpan( + text: + "${context.l10n.do_you_want_to_open_the_following_link}:\n", + ), + if (href != null) + TextSpan( + text: "$href\n\n", + style: const TextStyle(color: Colors.blue), + ), + TextSpan(text: context.l10n.unsafe_url_warning), + ], + ), + ), + actions: [ + Button.ghost( + onPressed: () => Navigator.of(context).pop(false), + child: Text(context.l10n.cancel), + ), + Button.ghost( + onPressed: () { + if (href != null) { + Clipboard.setData(ClipboardData(text: href!)); + } + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.copy_link), + ), + Button.destructive( + onPressed: () { + if (href != null) { + launchUrlString( + href!, + mode: LaunchMode.externalApplication, + ); + } + Navigator.of(context).pop(true); + }, + child: Text(context.l10n.open), + ), + ], + ), + ); + } +} diff --git a/lib/components/markdown/markdown.dart b/lib/components/markdown/markdown.dart index 9ea2e77c..1fd4ac5b 100644 --- a/lib/components/markdown/markdown.dart +++ b/lib/components/markdown/markdown.dart @@ -1,9 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/extensions/context.dart'; +import 'package:spotube/components/dialogs/link_open_permission_dialog.dart'; import 'package:url_launcher/url_launcher_string.dart'; class AppMarkdown extends StatelessWidget { @@ -28,61 +26,7 @@ class AppMarkdown extends StatelessWidget { final allowOpeningLink = await showDialog( context: context, builder: (context) { - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 450), - child: AlertDialog( - title: Row( - spacing: 8, - children: [ - const Icon(SpotubeIcons.warning), - Text(context.l10n.open_link_in_browser), - ], - ), - content: Text.rich( - TextSpan( - children: [ - TextSpan( - text: - "${context.l10n.do_you_want_to_open_the_following_link}:\n", - ), - if (href != null) - TextSpan( - text: "$href\n\n", - style: const TextStyle(color: Colors.blue), - ), - TextSpan(text: context.l10n.unsafe_url_warning), - ], - ), - ), - actions: [ - Button.ghost( - onPressed: () => Navigator.of(context).pop(false), - child: Text(context.l10n.cancel), - ), - Button.ghost( - onPressed: () { - if (href != null) { - Clipboard.setData(ClipboardData(text: href)); - } - Navigator.of(context).pop(false); - }, - child: Text(context.l10n.copy_link), - ), - Button.destructive( - onPressed: () { - if (href != null) { - launchUrlString( - href, - mode: LaunchMode.externalApplication, - ); - } - Navigator.of(context).pop(true); - }, - child: Text(context.l10n.open), - ), - ], - ), - ); + return LinkOpenPermissionDialog(href: href); }, ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 34b56489..de4a784b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -461,5 +461,6 @@ "available_plugins": "Available plugins", "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", "audio_scrobblers": "Audio Scrobblers", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "source": "Source: " } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index bf6f5211..79534e94 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2930,6 +2930,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Scrobbling'** String get scrobbling; + + /// No description provided for @source. + /// + /// In en, this message translates to: + /// **'Source: '** + String get source; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index b5734db6..e3d72f81 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1537,4 +1537,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get scrobbling => 'التتبع'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index 4503564e..cfa4d31a 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1538,4 +1538,7 @@ class AppLocalizationsBn extends AppLocalizations { @override String get scrobbling => 'স্ক্রোব্বলিং'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index 70899f04..458286dd 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1548,4 +1548,7 @@ class AppLocalizationsCa extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 3cb620e0..6c89a2ab 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1538,4 +1538,7 @@ class AppLocalizationsCs extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 75c858f5..1a79979c 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1550,4 +1550,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index e6d4db1e..a9125e56 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1536,4 +1536,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index f51f829c..ffa6eced 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1551,4 +1551,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 523f110f..b0a7be9a 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1548,4 +1548,7 @@ class AppLocalizationsEu extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index c63e723a..64f797f3 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1536,4 +1536,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get scrobbling => 'اسکراب‌بلینگ'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index e1ba7f5a..6f00104a 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1536,4 +1536,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 88350997..067e812d 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1556,4 +1556,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index f3ba4802..0ca1641f 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1542,4 +1542,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get scrobbling => 'स्क्रॉबलिंग'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index c56f1ece..5545f306 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1544,4 +1544,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index dc8ed9cd..b130f9b4 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1543,4 +1543,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 7ce62161..104cf201 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1507,4 +1507,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index a28bd02d..e44e9aa7 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1545,4 +1545,7 @@ class AppLocalizationsKa extends AppLocalizations { @override String get scrobbling => 'სქრობლინგი'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 40104b52..aac88524 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1511,4 +1511,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get scrobbling => '스크로블링'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 18d155fe..726a8eee 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1548,4 +1548,7 @@ class AppLocalizationsNe extends AppLocalizations { @override String get scrobbling => 'स्क्रब्बलिंग'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 3074e958..d0a64253 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1542,4 +1542,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 969204da..8d29354e 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1544,4 +1544,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 35d9881d..10a036af 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1541,4 +1541,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index e4cd090b..d0d6903d 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1544,4 +1544,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scrobbling => 'Скробблинг'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 0a131edd..20f89003 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1550,4 +1550,7 @@ class AppLocalizationsTa extends AppLocalizations { @override String get scrobbling => 'ஸ்க்ரோப்ளிங்'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 85230bfd..98ae0ad4 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1533,4 +1533,7 @@ class AppLocalizationsTh extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index 361a7bf0..0b9b79d5 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1551,4 +1551,7 @@ class AppLocalizationsTl extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 4dc65bbc..e5a36ba1 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1544,4 +1544,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 35a18d55..f0254780 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1540,4 +1540,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scrobbling => 'Скроблінг'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 6015931e..aa73fbdc 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1546,4 +1546,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index e42b6994..d3b25f71 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1500,6 +1500,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scrobbling => 'Scrobbling'; + + @override + String get source => 'Source: '; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/modules/metadata_plugins/plugin_repository.dart b/lib/modules/metadata_plugins/plugin_repository.dart index f140b9ee..295aed53 100644 --- a/lib/modules/metadata_plugins/plugin_repository.dart +++ b/lib/modules/metadata_plugins/plugin_repository.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -8,6 +9,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:change_case/change_case.dart'; class MetadataPluginRepositoryItem extends HookConsumerWidget { final MetadataPluginRepository pluginRepo; @@ -26,144 +28,180 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget { final isInstalling = useState(false); return Card( - child: Basic( - title: Text( - "${pluginRepo.owner == "KRTirtho" ? "" : "${pluginRepo.owner}/"}${pluginRepo.name}"), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Text(pluginRepo.description), - Row( - spacing: 8, - children: [ - if (pluginRepo.owner == "KRTirtho") ...[ - PrimaryBadge( - leading: Icon(SpotubeIcons.done), - child: Text(context.l10n.official), - ), - SecondaryBadge( - leading: host == "github.com" - ? const Icon(SpotubeIcons.github) - : null, - child: Text(host), - onPressed: () { - launchUrlString(pluginRepo.repoUrl); - }, - ), - ] else ...[ - Text(context.l10n.author_name(pluginRepo.owner)), - DestructiveBadge( - leading: const Icon(SpotubeIcons.warning), - child: Text(context.l10n.third_party), - ) - ] - ], + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + Basic( + title: Text( + pluginRepo.name.startsWith("spotube-plugin") + ? pluginRepo.name + .replaceFirst("spotube-plugin-", "") + .trim() + .toCapitalCase() + : pluginRepo.name.toCapitalCase(), ), - ], - ), - trailing: Button.primary( - enabled: !isInstalling.value, - onPressed: () async { - try { - isInstalling.value = true; - final pluginConfig = await pluginsNotifier - .downloadAndCachePlugin(pluginRepo.repoUrl); + subtitle: Text(pluginRepo.description), + trailing: Button.primary( + enabled: !isInstalling.value, + onPressed: () async { + try { + isInstalling.value = true; + final pluginConfig = await pluginsNotifier + .downloadAndCachePlugin(pluginRepo.repoUrl); - if (!context.mounted) return; - final isOfficialPlugin = pluginRepo.owner == "KRTirtho"; + if (!context.mounted) return; + final isOfficialPlugin = pluginRepo.owner == "KRTirtho"; - final isAllowed = isOfficialPlugin - ? true - : await showDialog( - context: context, - builder: (context) { - final pluginAbilities = pluginConfig.apis - .map( - (e) => context.l10n.can_access_name_api(e.name)) - .join("\n\n"); + final isAllowed = isOfficialPlugin + ? true + : await showDialog( + context: context, + builder: (context) { + final pluginAbilities = pluginConfig.apis + .map((e) => + context.l10n.can_access_name_api(e.name)) + .join("\n\n"); - return AlertDialog( - title: Text( - context.l10n.do_you_want_to_install_this_plugin), - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.third_party_plugin_warning), - const Gap(8), - FutureBuilder( - future: - pluginsNotifier.getLogoPath(pluginConfig), - builder: (context, snapshot) { - return Basic( - leading: snapshot.hasData - ? Image.file( - snapshot.data!, - width: 36, - height: 36, - ) - : Container( - height: 36, - width: 36, - alignment: Alignment.center, - decoration: BoxDecoration( - color: context - .theme.colorScheme.secondary, - borderRadius: - BorderRadius.circular(8), - ), - child: - const Icon(SpotubeIcons.plugin), - ), - title: Text(pluginConfig.name), - subtitle: Text(pluginConfig.description), - ); - }, + return AlertDialog( + title: Text(context + .l10n.do_you_want_to_install_this_plugin), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.third_party_plugin_warning), + const Gap(8), + FutureBuilder( + future: pluginsNotifier + .getLogoPath(pluginConfig), + builder: (context, snapshot) { + return Basic( + leading: snapshot.hasData + ? Image.file( + snapshot.data!, + width: 36, + height: 36, + ) + : Container( + height: 36, + width: 36, + alignment: Alignment.center, + decoration: BoxDecoration( + color: context.theme + .colorScheme.secondary, + borderRadius: + BorderRadius.circular(8), + ), + child: const Icon( + SpotubeIcons.plugin), + ), + title: Text(pluginConfig.name), + subtitle: + Text(pluginConfig.description), + ); + }, + ), + const Gap(8), + AppMarkdown( + data: + "**${context.l10n.author}**: ${pluginConfig.author}\n\n" + "**${context.l10n.repository}**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n" + "${context.l10n.this_plugin_can_do_following}:\n\n" + "$pluginAbilities", + ), + ], ), - const Gap(8), - AppMarkdown( - data: - "**${context.l10n.author}**: ${pluginConfig.author}\n\n" - "**${context.l10n.repository}**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n" - "${context.l10n.this_plugin_can_do_following}:\n\n" - "$pluginAbilities", - ), - ], - ), - actions: [ - Button.secondary( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(context.l10n.decline), - ), - Button.primary( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text(context.l10n.accept), - ), - ], + actions: [ + Button.secondary( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.decline), + ), + Button.primary( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(context.l10n.accept), + ), + ], + ); + }, ); - }, - ); - if (isAllowed != true) return; - await pluginsNotifier.addPlugin(pluginConfig); - } finally { - if (context.mounted) { - isInstalling.value = false; - } - } - }, - leading: isInstalling.value - ? const CircularProgressIndicator() - : const Icon(SpotubeIcons.add), - child: Text(context.l10n.install), - ), + if (isAllowed != true) return; + await pluginsNotifier.addPlugin(pluginConfig); + } finally { + if (context.mounted) { + isInstalling.value = false; + } + } + }, + leading: isInstalling.value + ? SizedBox.square( + dimension: 20, + child: CircularProgressIndicator( + color: context.theme.colorScheme.primaryForeground, + ), + ) + : const Icon(SpotubeIcons.add), + child: Text(context.l10n.install), + ), + ), + if (pluginRepo.owner != "KRTirtho") + Text.rich( + TextSpan( + children: [ + TextSpan(text: context.l10n.source), + TextSpan( + text: pluginRepo.repoUrl.replaceAll("https://", ""), + style: const TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + launchUrlString(pluginRepo.repoUrl); + }, + ), + ], + ), + style: context.theme.typography.xSmall, + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + if (pluginRepo.owner == "KRTirtho") + PrimaryBadge( + leading: const Icon(SpotubeIcons.done), + child: Text(context.l10n.official), + ) + else ...[ + Text( + context.l10n.author_name(pluginRepo.owner), + style: context.theme.typography.xSmall, + ), + DestructiveBadge( + leading: const Icon(SpotubeIcons.warning), + child: Text(context.l10n.third_party), + ), + ], + SecondaryBadge( + leading: host == "github.com" + ? const Icon(SpotubeIcons.github) + : null, + child: Text(host), + onPressed: () { + launchUrlString(pluginRepo.repoUrl); + }, + ), + ], + ), + ], ), ); } diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index b61c0255..881c0113 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -350,6 +350,8 @@ class MetadataPluginNotifier extends AsyncNotifier { abilities: plugin.abilities.map((e) => e.name).toList(), pluginApiVersion: Value(plugin.pluginApiVersion), repository: Value(plugin.repository), + // Setting the very first plugin as the default plugin + selected: Value(state.valueOrNull?.plugins.isEmpty ?? true), ), ); } @@ -362,6 +364,17 @@ class MetadataPluginNotifier extends AsyncNotifier { } await database.metadataPluginsTable.deleteWhere((tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)); + + // Same here, if the removed plugin is the default plugin + // set the first available plugin as the default plugin + // only when there is 1 remaining plugin + if (state.valueOrNull?.defaultPluginConfig == plugin) { + final remainingPlugins = + state.valueOrNull?.plugins.where((p) => p != plugin) ?? []; + if (remainingPlugins.length == 1) { + await setDefaultPlugin(remainingPlugins.first); + } + } } Future updatePlugin( diff --git a/pubspec.lock b/pubspec.lock index a6720c55..4d900033 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -315,7 +315,7 @@ packages: source: hosted version: "1.3.1" change_case: - dependency: transitive + dependency: "direct main" description: name: change_case sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 diff --git a/pubspec.yaml b/pubspec.yaml index c3044aad..9d3c1a3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -163,6 +163,7 @@ dependencies: get_it: ^8.0.3 flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 + change_case: ^1.1.0 dev_dependencies: build_runner: ^2.4.13 diff --git a/untranslated_messages.json b/untranslated_messages.json index af89bb78..08dfdf16 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,5 +1,118 @@ { + "ar": [ + "source" + ], + + "bn": [ + "source" + ], + + "ca": [ + "source" + ], + + "cs": [ + "source" + ], + + "de": [ + "source" + ], + + "es": [ + "source" + ], + + "eu": [ + "source" + ], + + "fa": [ + "source" + ], + + "fi": [ + "source" + ], + + "fr": [ + "source" + ], + + "hi": [ + "source" + ], + + "id": [ + "source" + ], + + "it": [ + "source" + ], + + "ja": [ + "source" + ], + + "ka": [ + "source" + ], + + "ko": [ + "source" + ], + + "ne": [ + "source" + ], + "nl": [ - "audio_source" + "audio_source", + "source" + ], + + "pl": [ + "source" + ], + + "pt": [ + "source" + ], + + "ru": [ + "source" + ], + + "ta": [ + "source" + ], + + "th": [ + "source" + ], + + "tl": [ + "source" + ], + + "tr": [ + "source" + ], + + "uk": [ + "source" + ], + + "vi": [ + "source" + ], + + "zh": [ + "source" + ], + + "zh_TW": [ + "source" ] } From e8a54d3209de4c3246e15ceb6ed21655e3e483fb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Sep 2025 10:31:49 +0600 Subject: [PATCH 06/47] feat(playback): add dab music source --- lib/models/database/tables/preferences.dart | 12 +- lib/models/database/tables/source_match.dart | 3 +- lib/pages/settings/sections/playback.dart | 6 +- lib/provider/server/routes/playback.dart | 19 +- lib/services/audio_player/audio_player.dart | 1 + lib/services/sourced_track/enums.dart | 3 +- lib/services/sourced_track/sourced_track.dart | 21 +- .../sourced_track/sources/dab_music.dart | 203 ++++++++++++++++++ pubspec.lock | 17 ++ pubspec.yaml | 4 + 10 files changed, 259 insertions(+), 30 deletions(-) create mode 100644 lib/services/sourced_track/sources/dab_music.dart diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 85014920..82c90c55 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -12,12 +12,14 @@ enum CloseBehavior { } enum AudioSource { - youtube, - piped, - jiosaavn, - invidious; + youtube("YouTube"), + piped("Piped"), + jiosaavn("JioSaavn"), + invidious("Invidious"), + dabMusic("DAB Music"); - String get label => name[0].toUpperCase() + name.substring(1); + final String label; + const AudioSource(this.label); } enum YoutubeClientEngine { diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index 78d0eb05..fa659287 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -3,7 +3,8 @@ part of '../database.dart'; enum SourceType { youtube._("YouTube"), youtubeMusic._("YouTube Music"), - jiosaavn._("JioSaavn"); + jiosaavn._("JioSaavn"), + dabMusic._("DAB Music"); final String label; diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 6acc70b1..2e1ddd27 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart' show ListTile; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/models/database/database.dart'; @@ -396,7 +396,9 @@ class SettingsPlaybackSection extends HookConsumerWidget { onChanged: preferencesNotifier.setNormalizeAudio, ), ), - if (preferences.audioSource != AudioSource.jiosaavn) ...[ + if (const [AudioSource.jiosaavn, AudioSource.dabMusic] + .contains(preferences.audioSource) == + false) ...[ AdaptiveSelectTile( popupConstraints: const BoxConstraints(maxWidth: 300), secondary: const Icon(SpotubeIcons.stream), diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index c81d968f..9e1d96a9 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -135,23 +135,6 @@ class ServerPlaybackRoutes { ); } - final contentLength = contentLengthRes?.headers.value("content-length"); - - /// Forcing partial content range as mpv sometimes greedily wants - /// everything at one go. Slows down overall streaming. - final range = RangeHeader.parse(headers["range"] ?? ""); - final contentPartialLength = int.tryParse(contentLength ?? ""); - if ((range.end == null) && - contentPartialLength != null && - range.start == 0) { - options = options.copyWith( - headers: { - ...?options.headers, - "range": "$range${(contentPartialLength * 0.3).ceil()}", - }, - ); - } - final res = await dio.get(url, options: options); final bytes = res.data; @@ -183,7 +166,7 @@ class ServerPlaybackRoutes { await trackPartialCacheFile.rename(trackCacheFile.path); } - if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) { + if (contentRange.total == fileLength && track.codec == SourceCodecs.m4a) { final playlistTrack = playlist.tracks.firstWhereOrNull( (element) => element.id == track.query.id, ); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 93a6417e..925d0761 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -56,6 +56,7 @@ abstract class AudioPlayerInterface { configuration: const mk.PlayerConfiguration( title: "Spotube", logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, + bufferSize: 4 * 1024 * 1024, // 4MB buffer ), ) { _mkPlayer.stream.error.listen((event) { diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart index d9ea079c..4c5ef8bc 100644 --- a/lib/services/sourced_track/enums.dart +++ b/lib/services/sourced_track/enums.dart @@ -2,7 +2,8 @@ import 'package:spotube/models/playback/track_sources.dart'; enum SourceCodecs { m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); + weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"), + mp3._("MP3 (Widely supported audio format)"); final String label; const SourceCodecs._(this.label); diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index d979c007..0d8d0a73 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -5,6 +5,7 @@ import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/sources/dab_music.dart'; import 'package:spotube/services/sourced_track/sources/invidious.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart'; @@ -74,6 +75,14 @@ abstract class SourcedTrack extends BasicSourcedTrack { query: query, sources: sources, ), + AudioSource.dabMusic => DABMusicSourcedTrack( + ref: ref, + source: source, + siblings: siblings, + info: info, + query: query, + sources: sources, + ), }; } @@ -104,6 +113,8 @@ abstract class SourcedTrack extends BasicSourcedTrack { await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref), AudioSource.jiosaavn => await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref), + AudioSource.dabMusic => + await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref), }; } catch (e) { if (preferences.audioSource == AudioSource.youtube) { @@ -129,6 +140,8 @@ abstract class SourcedTrack extends BasicSourcedTrack { JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref), AudioSource.invidious => InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref), + AudioSource.dabMusic => + DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref), }; } @@ -198,9 +211,11 @@ abstract class SourcedTrack extends BasicSourcedTrack { SourceCodecs get codec { final preferences = ref.read(userPreferencesProvider); - return preferences.audioSource == AudioSource.jiosaavn - ? SourceCodecs.m4a - : preferences.streamMusicCodec; + return switch (preferences.audioSource) { + AudioSource.dabMusic => SourceCodecs.mp3, + AudioSource.jiosaavn => SourceCodecs.m4a, + _ => preferences.streamMusicCodec + }; } TrackSource get activeTrackSource { diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart new file mode 100644 index 00000000..20ec68f1 --- /dev/null +++ b/lib/services/sourced_track/sources/dab_music.dart @@ -0,0 +1,203 @@ +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:dab_music_api/dab_music_api.dart'; + +final dabMusicApiClient = DabMusicApiClient( + Dio(), + baseUrl: "https://dab.yeet.su/api", +); + +/// Only Music source that can't support database caching due to having no endpoint. +/// But ISRC search is 100% reliable so caching is actually not necessary. +class DABMusicSourcedTrack extends SourcedTrack { + DABMusicSourcedTrack({ + required super.ref, + required super.source, + required super.siblings, + required super.info, + required super.query, + required super.sources, + }); + + static Future fetchFromTrack({ + required TrackSourceQuery query, + required Ref ref, + }) async { + try { + final siblings = await fetchSiblings(ref: ref, query: query); + + if (siblings.isEmpty) { + throw TrackNotFoundError(query); + } + return DABMusicSourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + sources: siblings.first.source!, + info: siblings.first.info, + query: query, + source: AudioSource.dabMusic, + ); + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + rethrow; + } + } + + static Future> fetchSources( + String id, + SourceQualities quality, + ) async { + try { + final streamResponse = await dabMusicApiClient.music.getStream( + trackId: id, + quality: "5", // mp3 320kbps (best available) + ); + if (streamResponse.url == null) { + throw Exception("No stream URL found for track ID: $id"); + } + return [ + TrackSource( + url: streamResponse.url!, + quality: SourceQualities.high, + bitrate: "320kbps", + codec: SourceCodecs.mp3, + ), + ]; + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + rethrow; + } + } + + static Future toSiblingType( + Ref ref, + int index, + Track result, + ) async { + try { + List? source; + if (index == 0) { + source = await fetchSources( + result.id.toString(), + ref.read(userPreferencesProvider).audioQuality, + ); + } + + final SiblingType sibling = ( + info: TrackSourceInfo( + artists: result.artist!, + durationMs: Duration(seconds: result.duration!).inMilliseconds, + id: result.id.toString(), + pageUrl: "https://dab.yeet.su/music/${result.id}", + thumbnail: result.albumCover!, + title: result.title!, + ), + source: source, + ); + + return sibling; + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + rethrow; + } + } + + static Future> fetchSiblings({ + required TrackSourceQuery query, + required Ref ref, + }) async { + try { + List results = []; + + if (query.isrc.isNotEmpty) { + final res = + await dabMusicApiClient.music.getSearch(q: query.isrc, limit: 1); + results = res.tracks ?? []; + } + + if (results.isEmpty) { + final res = await dabMusicApiClient.music.getSearch( + q: SourcedTrack.getSearchTerm(query), + limit: 20, + ); + results = res.tracks ?? []; + } + + if (results.isEmpty) { + return []; + } + + final matchedResults = + results.mapIndexed((index, d) => toSiblingType(ref, index, d)); + + return Future.wait(matchedResults); + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + rethrow; + } + } + + @override + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); + + return DABMusicSourcedTrack( + ref: ref, + siblings: fetchedSiblings + .where((s) => s.info.id != info.id) + .map((s) => s.info) + .toList(), + source: source, + info: info, + query: query, + sources: sources, + ); + } + + @override + Future swapWithSibling(TrackSourceInfo sibling) async { + if (sibling.id == this.info.id) { + return null; + } + + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, this.info); + + final source = await fetchSources( + sibling.id, + ref.read(userPreferencesProvider).audioQuality, + ); + + return DABMusicSourcedTrack( + ref: ref, + siblings: newSiblings, + sources: source, + info: newSourceInfo, + query: query, + source: AudioSource.dabMusic, + ); + } + + @override + Future refreshStream() async { + // There's no need to refresh the stream for DABMusicSourcedTrack + return this; + } +} diff --git a/pubspec.lock b/pubspec.lock index 4d900033..0ccd5c44 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -458,6 +458,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + dab_music_api: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "55f96368b7465eec2e5e81774f9f2a7b18acc4ab" + url: "https://github.com/KRTirtho/dab_music_api.git" + source: git + version: "0.1.0" dart_des: dependency: transitive description: @@ -2046,6 +2055,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + retrofit: + dependency: transitive + description: + name: retrofit + sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25" + url: "https://pub.dev" + source: hosted + version: "4.7.2" riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9d3c1a3c..0877d736 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,10 @@ dependencies: bonsoir: ^5.1.10 cached_network_image: ^3.3.1 connectivity_plus: ^6.1.2 + dab_music_api: + git: + url: https://github.com/KRTirtho/dab_music_api.git + ref: main desktop_webview_window: git: path: packages/desktop_webview_window From cecb687592b427045ec9c2de5f4f8d7d0429c1a9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Sep 2025 11:53:36 +0600 Subject: [PATCH 07/47] feat(playback): add uncompressed flac playback support --- cli/commands/install-dependencies.dart | 5 ++ lib/l10n/app_en.arb | 3 +- lib/l10n/generated/app_localizations.dart | 6 ++ lib/l10n/generated/app_localizations_ar.dart | 3 + lib/l10n/generated/app_localizations_bn.dart | 3 + lib/l10n/generated/app_localizations_ca.dart | 3 + lib/l10n/generated/app_localizations_cs.dart | 3 + lib/l10n/generated/app_localizations_de.dart | 3 + lib/l10n/generated/app_localizations_en.dart | 3 + lib/l10n/generated/app_localizations_es.dart | 3 + lib/l10n/generated/app_localizations_eu.dart | 3 + lib/l10n/generated/app_localizations_fa.dart | 3 + lib/l10n/generated/app_localizations_fi.dart | 3 + lib/l10n/generated/app_localizations_fr.dart | 3 + lib/l10n/generated/app_localizations_hi.dart | 3 + lib/l10n/generated/app_localizations_id.dart | 3 + lib/l10n/generated/app_localizations_it.dart | 3 + lib/l10n/generated/app_localizations_ja.dart | 3 + lib/l10n/generated/app_localizations_ka.dart | 3 + lib/l10n/generated/app_localizations_ko.dart | 3 + lib/l10n/generated/app_localizations_ne.dart | 3 + lib/l10n/generated/app_localizations_nl.dart | 3 + lib/l10n/generated/app_localizations_pl.dart | 3 + lib/l10n/generated/app_localizations_pt.dart | 3 + lib/l10n/generated/app_localizations_ru.dart | 3 + lib/l10n/generated/app_localizations_ta.dart | 3 + lib/l10n/generated/app_localizations_th.dart | 3 + lib/l10n/generated/app_localizations_tl.dart | 3 + lib/l10n/generated/app_localizations_tr.dart | 3 + lib/l10n/generated/app_localizations_uk.dart | 3 + lib/l10n/generated/app_localizations_vi.dart | 3 + lib/l10n/generated/app_localizations_zh.dart | 3 + lib/models/database/tables/preferences.dart | 8 -- lib/pages/settings/sections/playback.dart | 5 ++ lib/provider/server/routes/playback.dart | 22 +++-- .../user_preferences_provider.dart | 25 ++++++ .../audio_player/audio_player_impl.dart | 4 + lib/services/audio_player/custom_player.dart | 8 ++ lib/services/sourced_track/enums.dart | 4 +- lib/services/sourced_track/sourced_track.dart | 5 +- .../sourced_track/sources/dab_music.dart | 13 +-- untranslated_messages.json | 87 ++++++++++++------- 42 files changed, 232 insertions(+), 50 deletions(-) diff --git a/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart index e26b8078..336ffae7 100644 --- a/cli/commands/install-dependencies.dart +++ b/cli/commands/install-dependencies.dart @@ -39,6 +39,11 @@ class InstallDependenciesCommand extends Command { switch (argResults!.option("platform")) { case "windows": + await shell.run( + """ + choco install innosetup -y + """, + ); break; case "linux": await shell.run( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index de4a784b..228e24b1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -462,5 +462,6 @@ "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", "audio_scrobblers": "Audio Scrobblers", "scrobbling": "Scrobbling", - "source": "Source: " + "source": "Source: ", + "uncompressed": "Uncompressed" } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 79534e94..b2c94dc1 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2936,6 +2936,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Source: '** String get source; + + /// No description provided for @uncompressed. + /// + /// In en, this message translates to: + /// **'Uncompressed'** + String get uncompressed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index e3d72f81..cd2bc7e9 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1540,4 +1540,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index cfa4d31a..2246c64c 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1541,4 +1541,7 @@ class AppLocalizationsBn extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index 458286dd..c0d55c75 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1551,4 +1551,7 @@ class AppLocalizationsCa extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 6c89a2ab..2c1e4dfa 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1541,4 +1541,7 @@ class AppLocalizationsCs extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 1a79979c..12ce5e81 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1553,4 +1553,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index a9125e56..fdafac30 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1539,4 +1539,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index ffa6eced..b9d2bb5a 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1554,4 +1554,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index b0a7be9a..0ac13ddc 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1551,4 +1551,7 @@ class AppLocalizationsEu extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 64f797f3..140077c2 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1539,4 +1539,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 6f00104a..b1d13d8e 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1539,4 +1539,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 067e812d..6a7e51cb 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1559,4 +1559,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 0ca1641f..1be49d24 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1545,4 +1545,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 5545f306..225fb9a5 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1547,4 +1547,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index b130f9b4..88128c6c 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1546,4 +1546,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 104cf201..f444e4e4 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1510,4 +1510,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index e44e9aa7..a9efb949 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1548,4 +1548,7 @@ class AppLocalizationsKa extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index aac88524..c6cc39c8 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1514,4 +1514,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 726a8eee..6bd1214e 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1551,4 +1551,7 @@ class AppLocalizationsNe extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index d0a64253..e7d6ee40 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1545,4 +1545,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 8d29354e..fe0493c0 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1547,4 +1547,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 10a036af..082817ae 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1544,4 +1544,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index d0d6903d..6645d9ff 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1547,4 +1547,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 20f89003..98cff256 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1553,4 +1553,7 @@ class AppLocalizationsTa extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 98ae0ad4..0156180e 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1536,4 +1536,7 @@ class AppLocalizationsTh extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index 0b9b79d5..ddecd3cf 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1554,4 +1554,7 @@ class AppLocalizationsTl extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index e5a36ba1..6e9e36f2 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1547,4 +1547,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index f0254780..6d8e44b1 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1543,4 +1543,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index aa73fbdc..fcf545ae 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1549,4 +1549,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index d3b25f71..ec8a4e4b 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1503,6 +1503,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get source => 'Source: '; + + @override + String get uncompressed => 'Uncompressed'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 82c90c55..64580330 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -41,14 +41,6 @@ enum YoutubeClientEngine { } } -enum MusicCodec { - m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); - - final String label; - const MusicCodec._(this.label); -} - enum SearchMode { youtube._("YouTube"), youtubeMusic._("YouTube Music"); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 2e1ddd27..2d128742 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -44,6 +44,11 @@ class SettingsPlaybackSection extends HookConsumerWidget { title: Text(context.l10n.audio_quality), value: preferences.audioQuality, options: [ + if (preferences.audioSource == AudioSource.dabMusic) + SelectItemButton( + value: SourceQualities.uncompressed, + child: Text(context.l10n.uncompressed), + ), SelectItemButton( value: SourceQualities.high, child: Text(context.l10n.high), diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 9e1d96a9..ae51b080 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -101,10 +101,7 @@ class ServerPlaybackRoutes { ); final contentLengthRes = await Future.value( - dio.head( - url, - options: options, - ), + dio.head(url, options: options), ).catchError((e, stack) async { AppLogger.reportError(e, stack); @@ -135,6 +132,21 @@ class ServerPlaybackRoutes { ); } + if (headers["range"] == "bytes=0-") { + final bufferSize = + userPreferences.audioQuality == SourceQualities.uncompressed + ? 6 * 1024 * 1024 + : 4 * 1024 * 1024; + final endRange = min(bufferSize, + int.parse(contentLengthRes?.headers.value("content-length") ?? "0")); + options = options.copyWith( + headers: { + ...options.headers ?? {}, + "range": "bytes=0-$endRange", + }, + ); + } + final res = await dio.get(url, options: options); final bytes = res.data; @@ -166,7 +178,7 @@ class ServerPlaybackRoutes { await trackPartialCacheFile.rename(trackCacheFile.path); } - if (contentRange.total == fileLength && track.codec == SourceCodecs.m4a) { + if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) { final playlistTrack = playlist.tracks.firstWhereOrNull( (element) => element.id == track.query.id, ); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a5be97e2..8e8da561 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -54,6 +54,7 @@ class UserPreferencesNotifier extends Notifier { } await audioPlayer.setAudioNormalization(state.normalizeAudio); + await _updatePlayerBufferSize(event.audioQuality, state.audioQuality); } catch (e, stack) { AppLogger.reportError(e, stack); } @@ -79,6 +80,24 @@ class UserPreferencesNotifier extends Notifier { }); } + /// Sets audio player's buffer size based on the selected audio quality + /// Uncompressed quality gets a larger buffer size for smoother playback + /// while other qualities use a standard buffer size. + Future _updatePlayerBufferSize( + SourceQualities newQuality, + SourceQualities oldQuality, + ) async { + if (newQuality == SourceQualities.uncompressed) { + audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB + return; + } + + if (oldQuality == SourceQualities.uncompressed && + newQuality != SourceQualities.uncompressed) { + audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB + } + } + Future setData(PreferencesTableCompanion data) async { final db = ref.read(databaseProvider); @@ -155,6 +174,7 @@ class UserPreferencesNotifier extends Notifier { void setAudioQuality(SourceQualities quality) { setData(PreferencesTableCompanion(audioQuality: Value(quality))); + _updatePlayerBufferSize(quality, state.audioQuality); } void setDownloadLocation(String downloadDir) { @@ -204,6 +224,11 @@ class UserPreferencesNotifier extends Notifier { } void setAudioSource(AudioSource type) { + // Only allow uncompressed quality for DAB Music + if (type != AudioSource.dabMusic && + state.audioQuality == SourceQualities.uncompressed) { + setAudioQuality(SourceQualities.high); + } setData(PreferencesTableCompanion(audioSource: Value(type))); } diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 82c8c906..afd209a3 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -131,4 +131,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface Future setAudioNormalization(bool normalize) async { await _mkPlayer.setAudioNormalization(normalize); } + + Future setDemuxerBufferSize(int sizeInBytes) async { + await _mkPlayer.setDemuxerBufferSize(sizeInBytes); + } } diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index 5258696b..39866dcc 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -133,4 +133,12 @@ class CustomPlayer extends Player { await nativePlayer.setProperty('af', ''); } } + + Future setDemuxerBufferSize(int sizeInBytes) async { + await nativePlayer.setProperty('demuxer-max-bytes', sizeInBytes.toString()); + await nativePlayer.setProperty( + 'demuxer-max-back-bytes', + sizeInBytes.toString(), + ); + } } diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart index 4c5ef8bc..9a1a5040 100644 --- a/lib/services/sourced_track/enums.dart +++ b/lib/services/sourced_track/enums.dart @@ -3,13 +3,15 @@ import 'package:spotube/models/playback/track_sources.dart'; enum SourceCodecs { m4a._("M4a (Best for downloaded music)"), weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"), - mp3._("MP3 (Widely supported audio format)"); + mp3._("MP3 (Widely supported audio format)"), + flac._("FLAC (Lossless, best quality)\nLarge file size"); final String label; const SourceCodecs._(this.label); } enum SourceQualities { + uncompressed(3), high(2), medium(1), low(0); diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 0d8d0a73..a6abdb20 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -212,7 +212,10 @@ abstract class SourcedTrack extends BasicSourcedTrack { final preferences = ref.read(userPreferencesProvider); return switch (preferences.audioSource) { - AudioSource.dabMusic => SourceCodecs.mp3, + AudioSource.dabMusic => + preferences.audioQuality == SourceQualities.uncompressed + ? SourceCodecs.flac + : SourceCodecs.mp3, AudioSource.jiosaavn => SourceCodecs.m4a, _ => preferences.streamMusicCodec }; diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart index 20ec68f1..93293bd3 100644 --- a/lib/services/sourced_track/sources/dab_music.dart +++ b/lib/services/sourced_track/sources/dab_music.dart @@ -56,9 +56,10 @@ class DABMusicSourcedTrack extends SourcedTrack { SourceQualities quality, ) async { try { + final isUncompressed = quality == SourceQualities.uncompressed; final streamResponse = await dabMusicApiClient.music.getStream( trackId: id, - quality: "5", // mp3 320kbps (best available) + quality: isUncompressed ? "27" : "5", ); if (streamResponse.url == null) { throw Exception("No stream URL found for track ID: $id"); @@ -66,9 +67,11 @@ class DABMusicSourcedTrack extends SourcedTrack { return [ TrackSource( url: streamResponse.url!, - quality: SourceQualities.high, - bitrate: "320kbps", - codec: SourceCodecs.mp3, + quality: isUncompressed + ? SourceQualities.uncompressed + : SourceQualities.high, + bitrate: isUncompressed ? "2998kbps" : "320kbps", + codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3, ), ]; } catch (e, stackTrace) { @@ -126,7 +129,7 @@ class DABMusicSourcedTrack extends SourcedTrack { if (results.isEmpty) { final res = await dabMusicApiClient.music.getSearch( q: SourcedTrack.getSearchTerm(query), - limit: 20, + limit: 5, ); results = res.tracks ?? []; } diff --git a/untranslated_messages.json b/untranslated_messages.json index 08dfdf16..2334c1b3 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,118 +1,147 @@ { "ar": [ - "source" + "source", + "uncompressed" ], "bn": [ - "source" + "source", + "uncompressed" ], "ca": [ - "source" + "source", + "uncompressed" ], "cs": [ - "source" + "source", + "uncompressed" ], "de": [ - "source" + "source", + "uncompressed" ], "es": [ - "source" + "source", + "uncompressed" ], "eu": [ - "source" + "source", + "uncompressed" ], "fa": [ - "source" + "source", + "uncompressed" ], "fi": [ - "source" + "source", + "uncompressed" ], "fr": [ - "source" + "source", + "uncompressed" ], "hi": [ - "source" + "source", + "uncompressed" ], "id": [ - "source" + "source", + "uncompressed" ], "it": [ - "source" + "source", + "uncompressed" ], "ja": [ - "source" + "source", + "uncompressed" ], "ka": [ - "source" + "source", + "uncompressed" ], "ko": [ - "source" + "source", + "uncompressed" ], "ne": [ - "source" + "source", + "uncompressed" ], "nl": [ "audio_source", - "source" + "source", + "uncompressed" ], "pl": [ - "source" + "source", + "uncompressed" ], "pt": [ - "source" + "source", + "uncompressed" ], "ru": [ - "source" + "source", + "uncompressed" ], "ta": [ - "source" + "source", + "uncompressed" ], "th": [ - "source" + "source", + "uncompressed" ], "tl": [ - "source" + "source", + "uncompressed" ], "tr": [ - "source" + "source", + "uncompressed" ], "uk": [ - "source" + "source", + "uncompressed" ], "vi": [ - "source" + "source", + "uncompressed" ], "zh": [ - "source" + "source", + "uncompressed" ], "zh_TW": [ - "source" + "source", + "uncompressed" ] } From 3e34bc4be64169a697f682edd5cfc3534cd36839 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Sep 2025 21:40:26 +0600 Subject: [PATCH 08/47] chore: streaming issue for mp3 --- lib/provider/server/router.dart | 1 + lib/provider/server/routes/playback.dart | 153 +++++++++++++++++++---- 2 files changed, 127 insertions(+), 27 deletions(-) diff --git a/lib/provider/server/router.dart b/lib/provider/server/router.dart index 06ff4a24..f103ea8c 100644 --- a/lib/provider/server/router.dart +++ b/lib/provider/server/router.dart @@ -12,6 +12,7 @@ final serverRouterProvider = Provider((ref) { router.get("/ping", (Request request) => Response.ok("pong")); + router.head("/stream/", playbackRoutes.headStreamTrackId); router.get("/stream/", playbackRoutes.getStreamTrackId); router.get("/playback/toggle-playback", playbackRoutes.togglePlayback); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index ae51b080..4bce7444 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -46,21 +46,95 @@ class ServerPlaybackRoutes { ServerPlaybackRoutes(this.ref) : dio = Dio(); + Future _getTrackCacheFilePath(SourcedTrack track) async { + return join( + await UserPreferencesNotifier.getMusicCacheDir(), + ServiceUtils.sanitizeFilename( + '${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}', + ), + ); + } + + Future _getSourcedTrack( + Request request, String trackId) async { + final track = + playlist.tracks.firstWhere((element) => element.id == trackId); + + final activeSourcedTrack = + await ref.read(activeTrackSourcesProvider.future); + final sourcedTrack = activeSourcedTrack?.track.id == track.id + ? activeSourcedTrack?.source + : await ref.read( + trackSourcesProvider( + //! Use [Request.requestedUri] as it contains full https url. + //! [Request.url] will exclude and starts relatively. (streams/... basically) + TrackSourceQuery.parseUri(request.requestedUri.toString()), + ).future, + ); + + return sourcedTrack; + } + + Future streamTrackInformation( + Request request, + SourcedTrack track, + ) async { + AppLogger.log.i( + "HEAD request for track: ${track.query.title}\n" + "Headers: ${request.headers}", + ); + + final trackCacheFile = File(await _getTrackCacheFilePath(track)); + + if (await trackCacheFile.exists() && userPreferences.cacheMusic) { + final fileLength = await trackCacheFile.length(); + + return dio_lib.Response( + statusCode: 200, + headers: Headers.fromMap({ + "content-type": ["audio/${track.codec.name}"], + "content-length": ["$fileLength"], + "accept-ranges": ["bytes"], + "content-range": ["bytes 0-$fileLength/$fileLength"], + }), + requestOptions: RequestOptions(path: request.requestedUri.toString()), + ); + } + + String url = track.url ?? + await ref + .read(trackSourcesProvider(track.query).notifier) + .swapWithNextSibling() + .then((track) => track.url!); + + final options = Options( + headers: { + "user-agent": _randomUserAgent, + "Cache-Control": "max-age=3600", + "Connection": "keep-alive", + "host": Uri.parse(url).host, + }, + validateStatus: (status) => status! < 400, + ); + + final res = await dio.head(url, options: options); + + return res; + } + Future<({dio_lib.Response response, Uint8List? bytes})> streamTrack( Request request, SourcedTrack track, Map headers, ) async { - final trackCacheFile = File( - join( - await UserPreferencesNotifier.getMusicCacheDir(), - ServiceUtils.sanitizeFilename( - '${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}', - ), - ), + AppLogger.log.i( + "GET request for track: ${track.query.title}\n" + "Headers: ${request.headers}", ); + final trackCacheFile = File(await _getTrackCacheFilePath(track)); + if (await trackCacheFile.exists() && userPreferences.cacheMusic) { final bytes = await trackCacheFile.readAsBytes(); final cachedFileLength = bytes.length; @@ -132,16 +206,20 @@ class ServerPlaybackRoutes { ); } - if (headers["range"] == "bytes=0-") { + if (headers["range"] == "bytes=0-" && track.codec == SourceCodecs.flac) { final bufferSize = userPreferences.audioQuality == SourceQualities.uncompressed - ? 6 * 1024 * 1024 - : 4 * 1024 * 1024; - final endRange = min(bufferSize, - int.parse(contentLengthRes?.headers.value("content-length") ?? "0")); + ? 6 * 1024 * 1024 // 6MB for lossless + : 4 * 1024 * 1024; // 4MB for lossy + + final endRange = min( + bufferSize, + int.parse(contentLengthRes?.headers.value("content-length") ?? "0"), + ); + options = options.copyWith( headers: { - ...options.headers ?? {}, + ...?options.headers, "range": "bytes=0-$endRange", }, ); @@ -149,6 +227,12 @@ class ServerPlaybackRoutes { final res = await dio.get(url, options: options); + AppLogger.log.i( + "Response for track: ${track.query.title}\n" + "Status Code: ${res.statusCode}\n" + "Headers: ${res.headers.map}", + ); + final bytes = res.data; if (bytes == null || !userPreferences.cacheMusic) { @@ -208,27 +292,42 @@ class ServerPlaybackRoutes { return (bytes: bytes, response: res); } + /// @head('/stream/') + Future headStreamTrackId(Request request, String trackId) async { + try { + final sourcedTrack = await _getSourcedTrack(request, trackId); + + if (sourcedTrack == null) { + return Response.notFound("Track not found in the current queue"); + } + + final res = await streamTrackInformation( + request, + sourcedTrack, + ); + + return Response( + res.statusCode!, + headers: res.headers.map, + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + return Response.internalServerError(); + } + } + /// @get('/stream/') Future getStreamTrackId(Request request, String trackId) async { try { - final track = - playlist.tracks.firstWhere((element) => element.id == trackId); + final sourcedTrack = await _getSourcedTrack(request, trackId); - final activeSourcedTrack = - await ref.read(activeTrackSourcesProvider.future); - final sourcedTrack = activeSourcedTrack?.track.id == track.id - ? activeSourcedTrack?.source - : await ref.read( - trackSourcesProvider( - //! Use [Request.requestedUri] as it contains full https url. - //! [Request.url] will exclude and starts relatively. (streams/... basically) - TrackSourceQuery.parseUri(request.requestedUri.toString()), - ).future, - ); + if (sourcedTrack == null) { + return Response.notFound("Track not found in the current queue"); + } final (bytes: audioBytes, response: res) = await streamTrack( request, - sourcedTrack!, + sourcedTrack, request.headers, ); From 66848c78c71a71373881f6597a9de07be0d203c3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 19 Sep 2025 23:55:38 +0600 Subject: [PATCH 09/47] chore: add dab music option on getting started page and audio source quality label --- assets/images/logos/dab-music.png | Bin 0 -> 3703 bytes lib/collections/assets.gen.dart | 29 +- lib/collections/fonts.gen.dart | 3 +- lib/collections/routes.gr.dart | 344 +++++----------- lib/collections/spotube_icons.dart | 1 + lib/l10n/app_en.arb | 3 +- lib/l10n/generated/app_localizations.dart | 6 + lib/l10n/generated/app_localizations_ar.dart | 4 + lib/l10n/generated/app_localizations_bn.dart | 4 + lib/l10n/generated/app_localizations_ca.dart | 4 + lib/l10n/generated/app_localizations_cs.dart | 4 + lib/l10n/generated/app_localizations_de.dart | 4 + lib/l10n/generated/app_localizations_en.dart | 4 + lib/l10n/generated/app_localizations_es.dart | 4 + lib/l10n/generated/app_localizations_eu.dart | 4 + lib/l10n/generated/app_localizations_fa.dart | 4 + lib/l10n/generated/app_localizations_fi.dart | 4 + lib/l10n/generated/app_localizations_fr.dart | 4 + lib/l10n/generated/app_localizations_hi.dart | 4 + lib/l10n/generated/app_localizations_id.dart | 4 + lib/l10n/generated/app_localizations_it.dart | 4 + lib/l10n/generated/app_localizations_ja.dart | 4 + lib/l10n/generated/app_localizations_ka.dart | 4 + lib/l10n/generated/app_localizations_ko.dart | 4 + lib/l10n/generated/app_localizations_ne.dart | 4 + lib/l10n/generated/app_localizations_nl.dart | 4 + lib/l10n/generated/app_localizations_pl.dart | 4 + lib/l10n/generated/app_localizations_pt.dart | 4 + lib/l10n/generated/app_localizations_ru.dart | 4 + lib/l10n/generated/app_localizations_ta.dart | 4 + lib/l10n/generated/app_localizations_th.dart | 4 + lib/l10n/generated/app_localizations_tl.dart | 4 + lib/l10n/generated/app_localizations_tr.dart | 4 + lib/l10n/generated/app_localizations_uk.dart | 4 + lib/l10n/generated/app_localizations_vi.dart | 4 + lib/l10n/generated/app_localizations_zh.dart | 4 + lib/models/connect/connect.freezed.dart | 39 +- lib/models/database/database.g.dart | 72 ---- lib/models/metadata/metadata.freezed.dart | 370 ++++++++++++++---- lib/models/playback/track_sources.dart | 1 + .../playback/track_sources.freezed.dart | 99 ++++- lib/models/playback/track_sources.g.dart | 6 + .../metadata_plugins/installed_plugin.dart | 25 +- .../metadata_plugins/plugin_repository.dart | 28 +- lib/modules/player/player.dart | 23 ++ .../getting_started/sections/playback.dart | 50 +-- lib/pages/settings/sections/playback.dart | 18 +- lib/provider/audio_player/state.freezed.dart | 21 +- .../user_preferences_provider.dart | 20 +- lib/services/song_link/song_link.freezed.dart | 21 +- lib/services/sourced_track/sourced_track.dart | 24 +- .../sourced_track/sources/dab_music.dart | 21 +- .../sourced_track/sources/invidious.dart | 8 +- .../sourced_track/sources/jiosaavn.dart | 1 + lib/services/sourced_track/sources/piped.dart | 8 +- .../sourced_track/sources/youtube.dart | 8 +- untranslated_messages.json | 87 ++-- 57 files changed, 920 insertions(+), 532 deletions(-) create mode 100644 assets/images/logos/dab-music.png diff --git a/assets/images/logos/dab-music.png b/assets/images/logos/dab-music.png new file mode 100644 index 0000000000000000000000000000000000000000..e09d34104a5864aa1d101e87ae53212d146d7654 GIT binary patch literal 3703 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 literal 0 HcmV?d00001 diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index 31fb54b8..7fa75e1d 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -1,3 +1,5 @@ +// dart format width=80 + /// GENERATED CODE - DO NOT MODIFY BY HAND /// ***************************************************** /// FlutterGen @@ -5,7 +7,7 @@ // coverage:ignore-file // ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use +// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import import 'package:flutter/widgets.dart'; @@ -67,6 +69,10 @@ class $AssetsImagesGen { class $AssetsImagesLogosGen { const $AssetsImagesLogosGen(); + /// File path: assets/images/logos/dab-music.png + AssetGenImage get dabMusic => + const AssetGenImage('assets/images/logos/dab-music.png'); + /// File path: assets/images/logos/invidious.jpg AssetGenImage get invidious => const AssetGenImage('assets/images/logos/invidious.jpg'); @@ -80,11 +86,12 @@ class $AssetsImagesLogosGen { const AssetGenImage('assets/images/logos/songlink-transparent.png'); /// List of all assets - List get values => [invidious, jiosaavn, songlinkTransparent]; + List get values => + [dabMusic, invidious, jiosaavn, songlinkTransparent]; } class Assets { - Assets._(); + const Assets._(); static const String license = 'LICENSE'; static const $AssetsBrandingGen branding = $AssetsBrandingGen(); @@ -99,12 +106,14 @@ class AssetGenImage { this._assetName, { this.size, this.flavors = const {}, + this.animation, }); final String _assetName; final Size? size; final Set flavors; + final AssetGenImageAnimation? animation; Image image({ Key? key, @@ -127,7 +136,7 @@ class AssetGenImage { bool gaplessPlayback = true, bool isAntiAlias = false, String? package, - FilterQuality filterQuality = FilterQuality.low, + FilterQuality filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, }) { @@ -174,3 +183,15 @@ class AssetGenImage { String get keyName => _assetName; } + +class AssetGenImageAnimation { + const AssetGenImageAnimation({ + required this.isAnimation, + required this.duration, + required this.frames, + }); + + final bool isAnimation; + final Duration duration; + final int frames; +} diff --git a/lib/collections/fonts.gen.dart b/lib/collections/fonts.gen.dart index 16cc6e82..d2c68231 100644 --- a/lib/collections/fonts.gen.dart +++ b/lib/collections/fonts.gen.dart @@ -1,3 +1,4 @@ +// dart format width=80 /// GENERATED CODE - DO NOT MODIFY BY HAND /// ***************************************************** /// FlutterGen @@ -5,7 +6,7 @@ // coverage:ignore-file // ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use +// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import class FontFamily { FontFamily._(); diff --git a/lib/collections/routes.gr.dart b/lib/collections/routes.gr.dart index e039abb9..f5ff24bf 100644 --- a/lib/collections/routes.gr.dart +++ b/lib/collections/routes.gr.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** @@ -59,10 +60,7 @@ import 'package:spotube/pages/track/track.dart' as _i35; /// [_i1.AboutSpotubePage] class AboutSpotubeRoute extends _i41.PageRouteInfo { const AboutSpotubeRoute({List<_i41.PageRouteInfo>? children}) - : super( - AboutSpotubeRoute.name, - initialChildren: children, - ); + : super(AboutSpotubeRoute.name, initialChildren: children); static const String name = 'AboutSpotubeRoute'; @@ -83,15 +81,11 @@ class AlbumRoute extends _i41.PageRouteInfo { required _i43.SpotubeSimpleAlbumObject album, List<_i41.PageRouteInfo>? children, }) : super( - AlbumRoute.name, - args: AlbumRouteArgs( - key: key, - id: id, - album: album, - ), - rawPathParams: {'id': id}, - initialChildren: children, - ); + AlbumRoute.name, + args: AlbumRouteArgs(key: key, id: id, album: album), + rawPathParams: {'id': id}, + initialChildren: children, + ); static const String name = 'AlbumRoute'; @@ -99,21 +93,13 @@ class AlbumRoute extends _i41.PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return _i2.AlbumPage( - key: args.key, - id: args.id, - album: args.album, - ); + return _i2.AlbumPage(key: args.key, id: args.id, album: args.album); }, ); } class AlbumRouteArgs { - const AlbumRouteArgs({ - this.key, - required this.id, - required this.album, - }); + const AlbumRouteArgs({this.key, required this.id, required this.album}); final _i42.Key? key; @@ -135,14 +121,11 @@ class ArtistRoute extends _i41.PageRouteInfo { _i42.Key? key, List<_i41.PageRouteInfo>? children, }) : super( - ArtistRoute.name, - args: ArtistRouteArgs( - artistId: artistId, - key: key, - ), - rawPathParams: {'id': artistId}, - initialChildren: children, - ); + ArtistRoute.name, + args: ArtistRouteArgs(artistId: artistId, key: key), + rawPathParams: {'id': artistId}, + initialChildren: children, + ); static const String name = 'ArtistRoute'; @@ -151,20 +134,15 @@ class ArtistRoute extends _i41.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => ArtistRouteArgs(artistId: pathParams.getString('id'))); - return _i3.ArtistPage( - args.artistId, - key: args.key, + orElse: () => ArtistRouteArgs(artistId: pathParams.getString('id')), ); + return _i3.ArtistPage(args.artistId, key: args.key); }, ); } class ArtistRouteArgs { - const ArtistRouteArgs({ - required this.artistId, - this.key, - }); + const ArtistRouteArgs({required this.artistId, this.key}); final String artistId; @@ -180,10 +158,7 @@ class ArtistRouteArgs { /// [_i4.BlackListPage] class BlackListRoute extends _i41.PageRouteInfo { const BlackListRoute({List<_i41.PageRouteInfo>? children}) - : super( - BlackListRoute.name, - initialChildren: children, - ); + : super(BlackListRoute.name, initialChildren: children); static const String name = 'BlackListRoute'; @@ -199,10 +174,7 @@ class BlackListRoute extends _i41.PageRouteInfo { /// [_i5.ConnectControlPage] class ConnectControlRoute extends _i41.PageRouteInfo { const ConnectControlRoute({List<_i41.PageRouteInfo>? children}) - : super( - ConnectControlRoute.name, - initialChildren: children, - ); + : super(ConnectControlRoute.name, initialChildren: children); static const String name = 'ConnectControlRoute'; @@ -218,10 +190,7 @@ class ConnectControlRoute extends _i41.PageRouteInfo { /// [_i6.ConnectPage] class ConnectRoute extends _i41.PageRouteInfo { const ConnectRoute({List<_i41.PageRouteInfo>? children}) - : super( - ConnectRoute.name, - initialChildren: children, - ); + : super(ConnectRoute.name, initialChildren: children); static const String name = 'ConnectRoute'; @@ -237,10 +206,7 @@ class ConnectRoute extends _i41.PageRouteInfo { /// [_i7.GettingStartedPage] class GettingStartedRoute extends _i41.PageRouteInfo { const GettingStartedRoute({List<_i41.PageRouteInfo>? children}) - : super( - GettingStartedRoute.name, - initialChildren: children, - ); + : super(GettingStartedRoute.name, initialChildren: children); static const String name = 'GettingStartedRoute'; @@ -262,15 +228,15 @@ class HomeBrowseSectionItemsRoute required _i43.SpotubeBrowseSectionObject section, List<_i41.PageRouteInfo>? children, }) : super( - HomeBrowseSectionItemsRoute.name, - args: HomeBrowseSectionItemsRouteArgs( - key: key, - sectionId: sectionId, - section: section, - ), - rawPathParams: {'sectionId': sectionId}, - initialChildren: children, - ); + HomeBrowseSectionItemsRoute.name, + args: HomeBrowseSectionItemsRouteArgs( + key: key, + sectionId: sectionId, + section: section, + ), + rawPathParams: {'sectionId': sectionId}, + initialChildren: children, + ); static const String name = 'HomeBrowseSectionItemsRoute'; @@ -310,10 +276,7 @@ class HomeBrowseSectionItemsRouteArgs { /// [_i9.HomePage] class HomeRoute extends _i41.PageRouteInfo { const HomeRoute({List<_i41.PageRouteInfo>? children}) - : super( - HomeRoute.name, - initialChildren: children, - ); + : super(HomeRoute.name, initialChildren: children); static const String name = 'HomeRoute'; @@ -329,10 +292,7 @@ class HomeRoute extends _i41.PageRouteInfo { /// [_i10.LastFMLoginPage] class LastFMLoginRoute extends _i41.PageRouteInfo { const LastFMLoginRoute({List<_i41.PageRouteInfo>? children}) - : super( - LastFMLoginRoute.name, - initialChildren: children, - ); + : super(LastFMLoginRoute.name, initialChildren: children); static const String name = 'LastFMLoginRoute'; @@ -348,10 +308,7 @@ class LastFMLoginRoute extends _i41.PageRouteInfo { /// [_i11.LibraryPage] class LibraryRoute extends _i41.PageRouteInfo { const LibraryRoute({List<_i41.PageRouteInfo>? children}) - : super( - LibraryRoute.name, - initialChildren: children, - ); + : super(LibraryRoute.name, initialChildren: children); static const String name = 'LibraryRoute'; @@ -371,13 +328,10 @@ class LikedPlaylistRoute extends _i41.PageRouteInfo { required _i43.SpotubeSimplePlaylistObject playlist, List<_i41.PageRouteInfo>? children, }) : super( - LikedPlaylistRoute.name, - args: LikedPlaylistRouteArgs( - key: key, - playlist: playlist, - ), - initialChildren: children, - ); + LikedPlaylistRoute.name, + args: LikedPlaylistRouteArgs(key: key, playlist: playlist), + initialChildren: children, + ); static const String name = 'LikedPlaylistRoute'; @@ -385,19 +339,13 @@ class LikedPlaylistRoute extends _i41.PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return _i12.LikedPlaylistPage( - key: args.key, - playlist: args.playlist, - ); + return _i12.LikedPlaylistPage(key: args.key, playlist: args.playlist); }, ); } class LikedPlaylistRouteArgs { - const LikedPlaylistRouteArgs({ - this.key, - required this.playlist, - }); + const LikedPlaylistRouteArgs({this.key, required this.playlist}); final _i42.Key? key; @@ -419,15 +367,15 @@ class LocalLibraryRoute extends _i41.PageRouteInfo { bool isCache = false, List<_i41.PageRouteInfo>? children, }) : super( - LocalLibraryRoute.name, - args: LocalLibraryRouteArgs( - location: location, - key: key, - isDownloads: isDownloads, - isCache: isCache, - ), - initialChildren: children, - ); + LocalLibraryRoute.name, + args: LocalLibraryRouteArgs( + location: location, + key: key, + isDownloads: isDownloads, + isCache: isCache, + ), + initialChildren: children, + ); static const String name = 'LocalLibraryRoute'; @@ -471,10 +419,7 @@ class LocalLibraryRouteArgs { /// [_i14.LogsPage] class LogsRoute extends _i41.PageRouteInfo { const LogsRoute({List<_i41.PageRouteInfo>? children}) - : super( - LogsRoute.name, - initialChildren: children, - ); + : super(LogsRoute.name, initialChildren: children); static const String name = 'LogsRoute'; @@ -490,10 +435,7 @@ class LogsRoute extends _i41.PageRouteInfo { /// [_i15.LyricsPage] class LyricsRoute extends _i41.PageRouteInfo { const LyricsRoute({List<_i41.PageRouteInfo>? children}) - : super( - LyricsRoute.name, - initialChildren: children, - ); + : super(LyricsRoute.name, initialChildren: children); static const String name = 'LyricsRoute'; @@ -513,13 +455,10 @@ class MiniLyricsRoute extends _i41.PageRouteInfo { required _i44.Size prevSize, List<_i41.PageRouteInfo>? children, }) : super( - MiniLyricsRoute.name, - args: MiniLyricsRouteArgs( - key: key, - prevSize: prevSize, - ), - initialChildren: children, - ); + MiniLyricsRoute.name, + args: MiniLyricsRouteArgs(key: key, prevSize: prevSize), + initialChildren: children, + ); static const String name = 'MiniLyricsRoute'; @@ -527,19 +466,13 @@ class MiniLyricsRoute extends _i41.PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return _i16.MiniLyricsPage( - key: args.key, - prevSize: args.prevSize, - ); + return _i16.MiniLyricsPage(key: args.key, prevSize: args.prevSize); }, ); } class MiniLyricsRouteArgs { - const MiniLyricsRouteArgs({ - this.key, - required this.prevSize, - }); + const MiniLyricsRouteArgs({this.key, required this.prevSize}); final _i44.Key? key; @@ -555,10 +488,7 @@ class MiniLyricsRouteArgs { /// [_i17.PlayerLyricsPage] class PlayerLyricsRoute extends _i41.PageRouteInfo { const PlayerLyricsRoute({List<_i41.PageRouteInfo>? children}) - : super( - PlayerLyricsRoute.name, - initialChildren: children, - ); + : super(PlayerLyricsRoute.name, initialChildren: children); static const String name = 'PlayerLyricsRoute'; @@ -574,10 +504,7 @@ class PlayerLyricsRoute extends _i41.PageRouteInfo { /// [_i18.PlayerQueuePage] class PlayerQueueRoute extends _i41.PageRouteInfo { const PlayerQueueRoute({List<_i41.PageRouteInfo>? children}) - : super( - PlayerQueueRoute.name, - initialChildren: children, - ); + : super(PlayerQueueRoute.name, initialChildren: children); static const String name = 'PlayerQueueRoute'; @@ -593,10 +520,7 @@ class PlayerQueueRoute extends _i41.PageRouteInfo { /// [_i19.PlayerTrackSourcesPage] class PlayerTrackSourcesRoute extends _i41.PageRouteInfo { const PlayerTrackSourcesRoute({List<_i41.PageRouteInfo>? children}) - : super( - PlayerTrackSourcesRoute.name, - initialChildren: children, - ); + : super(PlayerTrackSourcesRoute.name, initialChildren: children); static const String name = 'PlayerTrackSourcesRoute'; @@ -617,15 +541,11 @@ class PlaylistRoute extends _i41.PageRouteInfo { required _i43.SpotubeSimplePlaylistObject playlist, List<_i41.PageRouteInfo>? children, }) : super( - PlaylistRoute.name, - args: PlaylistRouteArgs( - key: key, - id: id, - playlist: playlist, - ), - rawPathParams: {'id': id}, - initialChildren: children, - ); + PlaylistRoute.name, + args: PlaylistRouteArgs(key: key, id: id, playlist: playlist), + rawPathParams: {'id': id}, + initialChildren: children, + ); static const String name = 'PlaylistRoute'; @@ -643,11 +563,7 @@ class PlaylistRoute extends _i41.PageRouteInfo { } class PlaylistRouteArgs { - const PlaylistRouteArgs({ - this.key, - required this.id, - required this.playlist, - }); + const PlaylistRouteArgs({this.key, required this.id, required this.playlist}); final _i42.Key? key; @@ -665,10 +581,7 @@ class PlaylistRouteArgs { /// [_i21.ProfilePage] class ProfileRoute extends _i41.PageRouteInfo { const ProfileRoute({List<_i41.PageRouteInfo>? children}) - : super( - ProfileRoute.name, - initialChildren: children, - ); + : super(ProfileRoute.name, initialChildren: children); static const String name = 'ProfileRoute'; @@ -684,10 +597,7 @@ class ProfileRoute extends _i41.PageRouteInfo { /// [_i22.RootAppPage] class RootAppRoute extends _i41.PageRouteInfo { const RootAppRoute({List<_i41.PageRouteInfo>? children}) - : super( - RootAppRoute.name, - initialChildren: children, - ); + : super(RootAppRoute.name, initialChildren: children); static const String name = 'RootAppRoute'; @@ -703,10 +613,7 @@ class RootAppRoute extends _i41.PageRouteInfo { /// [_i23.SearchPage] class SearchRoute extends _i41.PageRouteInfo { const SearchRoute({List<_i41.PageRouteInfo>? children}) - : super( - SearchRoute.name, - initialChildren: children, - ); + : super(SearchRoute.name, initialChildren: children); static const String name = 'SearchRoute'; @@ -728,14 +635,14 @@ class SettingsMetadataProviderFormRoute required List<_i43.MetadataFormFieldObject> fields, List<_i41.PageRouteInfo>? children, }) : super( - SettingsMetadataProviderFormRoute.name, - args: SettingsMetadataProviderFormRouteArgs( - key: key, - title: title, - fields: fields, - ), - initialChildren: children, - ); + SettingsMetadataProviderFormRoute.name, + args: SettingsMetadataProviderFormRouteArgs( + key: key, + title: title, + fields: fields, + ), + initialChildren: children, + ); static const String name = 'SettingsMetadataProviderFormRoute'; @@ -775,10 +682,7 @@ class SettingsMetadataProviderFormRouteArgs { /// [_i25.SettingsMetadataProviderPage] class SettingsMetadataProviderRoute extends _i41.PageRouteInfo { const SettingsMetadataProviderRoute({List<_i41.PageRouteInfo>? children}) - : super( - SettingsMetadataProviderRoute.name, - initialChildren: children, - ); + : super(SettingsMetadataProviderRoute.name, initialChildren: children); static const String name = 'SettingsMetadataProviderRoute'; @@ -794,10 +698,7 @@ class SettingsMetadataProviderRoute extends _i41.PageRouteInfo { /// [_i26.SettingsPage] class SettingsRoute extends _i41.PageRouteInfo { const SettingsRoute({List<_i41.PageRouteInfo>? children}) - : super( - SettingsRoute.name, - initialChildren: children, - ); + : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; @@ -813,10 +714,7 @@ class SettingsRoute extends _i41.PageRouteInfo { /// [_i27.SettingsScrobblingPage] class SettingsScrobblingRoute extends _i41.PageRouteInfo { const SettingsScrobblingRoute({List<_i41.PageRouteInfo>? children}) - : super( - SettingsScrobblingRoute.name, - initialChildren: children, - ); + : super(SettingsScrobblingRoute.name, initialChildren: children); static const String name = 'SettingsScrobblingRoute'; @@ -832,10 +730,7 @@ class SettingsScrobblingRoute extends _i41.PageRouteInfo { /// [_i28.StatsAlbumsPage] class StatsAlbumsRoute extends _i41.PageRouteInfo { const StatsAlbumsRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsAlbumsRoute.name, - initialChildren: children, - ); + : super(StatsAlbumsRoute.name, initialChildren: children); static const String name = 'StatsAlbumsRoute'; @@ -851,10 +746,7 @@ class StatsAlbumsRoute extends _i41.PageRouteInfo { /// [_i29.StatsArtistsPage] class StatsArtistsRoute extends _i41.PageRouteInfo { const StatsArtistsRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsArtistsRoute.name, - initialChildren: children, - ); + : super(StatsArtistsRoute.name, initialChildren: children); static const String name = 'StatsArtistsRoute'; @@ -870,10 +762,7 @@ class StatsArtistsRoute extends _i41.PageRouteInfo { /// [_i30.StatsMinutesPage] class StatsMinutesRoute extends _i41.PageRouteInfo { const StatsMinutesRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsMinutesRoute.name, - initialChildren: children, - ); + : super(StatsMinutesRoute.name, initialChildren: children); static const String name = 'StatsMinutesRoute'; @@ -889,10 +778,7 @@ class StatsMinutesRoute extends _i41.PageRouteInfo { /// [_i31.StatsPage] class StatsRoute extends _i41.PageRouteInfo { const StatsRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsRoute.name, - initialChildren: children, - ); + : super(StatsRoute.name, initialChildren: children); static const String name = 'StatsRoute'; @@ -908,10 +794,7 @@ class StatsRoute extends _i41.PageRouteInfo { /// [_i32.StatsPlaylistsPage] class StatsPlaylistsRoute extends _i41.PageRouteInfo { const StatsPlaylistsRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsPlaylistsRoute.name, - initialChildren: children, - ); + : super(StatsPlaylistsRoute.name, initialChildren: children); static const String name = 'StatsPlaylistsRoute'; @@ -927,10 +810,7 @@ class StatsPlaylistsRoute extends _i41.PageRouteInfo { /// [_i33.StatsStreamFeesPage] class StatsStreamFeesRoute extends _i41.PageRouteInfo { const StatsStreamFeesRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsStreamFeesRoute.name, - initialChildren: children, - ); + : super(StatsStreamFeesRoute.name, initialChildren: children); static const String name = 'StatsStreamFeesRoute'; @@ -946,10 +826,7 @@ class StatsStreamFeesRoute extends _i41.PageRouteInfo { /// [_i34.StatsStreamsPage] class StatsStreamsRoute extends _i41.PageRouteInfo { const StatsStreamsRoute({List<_i41.PageRouteInfo>? children}) - : super( - StatsStreamsRoute.name, - initialChildren: children, - ); + : super(StatsStreamsRoute.name, initialChildren: children); static const String name = 'StatsStreamsRoute'; @@ -969,14 +846,11 @@ class TrackRoute extends _i41.PageRouteInfo { required String trackId, List<_i41.PageRouteInfo>? children, }) : super( - TrackRoute.name, - args: TrackRouteArgs( - key: key, - trackId: trackId, - ), - rawPathParams: {'id': trackId}, - initialChildren: children, - ); + TrackRoute.name, + args: TrackRouteArgs(key: key, trackId: trackId), + rawPathParams: {'id': trackId}, + initialChildren: children, + ); static const String name = 'TrackRoute'; @@ -985,20 +859,15 @@ class TrackRoute extends _i41.PageRouteInfo { builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( - orElse: () => TrackRouteArgs(trackId: pathParams.getString('id'))); - return _i35.TrackPage( - key: args.key, - trackId: args.trackId, + orElse: () => TrackRouteArgs(trackId: pathParams.getString('id')), ); + return _i35.TrackPage(key: args.key, trackId: args.trackId); }, ); } class TrackRouteArgs { - const TrackRouteArgs({ - this.key, - required this.trackId, - }); + const TrackRouteArgs({this.key, required this.trackId}); final _i44.Key? key; @@ -1014,10 +883,7 @@ class TrackRouteArgs { /// [_i36.UserAlbumsPage] class UserAlbumsRoute extends _i41.PageRouteInfo { const UserAlbumsRoute({List<_i41.PageRouteInfo>? children}) - : super( - UserAlbumsRoute.name, - initialChildren: children, - ); + : super(UserAlbumsRoute.name, initialChildren: children); static const String name = 'UserAlbumsRoute'; @@ -1033,10 +899,7 @@ class UserAlbumsRoute extends _i41.PageRouteInfo { /// [_i37.UserArtistsPage] class UserArtistsRoute extends _i41.PageRouteInfo { const UserArtistsRoute({List<_i41.PageRouteInfo>? children}) - : super( - UserArtistsRoute.name, - initialChildren: children, - ); + : super(UserArtistsRoute.name, initialChildren: children); static const String name = 'UserArtistsRoute'; @@ -1052,10 +915,7 @@ class UserArtistsRoute extends _i41.PageRouteInfo { /// [_i38.UserDownloadsPage] class UserDownloadsRoute extends _i41.PageRouteInfo { const UserDownloadsRoute({List<_i41.PageRouteInfo>? children}) - : super( - UserDownloadsRoute.name, - initialChildren: children, - ); + : super(UserDownloadsRoute.name, initialChildren: children); static const String name = 'UserDownloadsRoute'; @@ -1071,10 +931,7 @@ class UserDownloadsRoute extends _i41.PageRouteInfo { /// [_i39.UserLocalLibraryPage] class UserLocalLibraryRoute extends _i41.PageRouteInfo { const UserLocalLibraryRoute({List<_i41.PageRouteInfo>? children}) - : super( - UserLocalLibraryRoute.name, - initialChildren: children, - ); + : super(UserLocalLibraryRoute.name, initialChildren: children); static const String name = 'UserLocalLibraryRoute'; @@ -1090,10 +947,7 @@ class UserLocalLibraryRoute extends _i41.PageRouteInfo { /// [_i40.UserPlaylistsPage] class UserPlaylistsRoute extends _i41.PageRouteInfo { const UserPlaylistsRoute({List<_i41.PageRouteInfo>? children}) - : super( - UserPlaylistsRoute.name, - initialChildren: children, - ); + : super(UserPlaylistsRoute.name, initialChildren: children); static const String name = 'UserPlaylistsRoute'; diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index b10ef7e3..21cf4176 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -80,6 +80,7 @@ abstract class SpotubeIcons { static const hoverOff = Icons.back_hand_outlined; static const dragHandle = Icons.drag_indicator; static const lightning = Icons.flash_on_rounded; + static const lightningOutlined = FeatherIcons.zap; static const colorSync = FeatherIcons.activity; static const language = FeatherIcons.globe; static const error = FeatherIcons.alertTriangle; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 228e24b1..833fa724 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -463,5 +463,6 @@ "audio_scrobblers": "Audio Scrobblers", "scrobbling": "Scrobbling", "source": "Source: ", - "uncompressed": "Uncompressed" + "uncompressed": "Uncompressed", + "dab_music_source_description": "For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching." } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index b2c94dc1..d3124728 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2942,6 +2942,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Uncompressed'** String get uncompressed; + + /// No description provided for @dab_music_source_description. + /// + /// In en, this message translates to: + /// **'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'** + String get dab_music_source_description; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index cd2bc7e9..b974d2e4 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1543,4 +1543,8 @@ class AppLocalizationsAr extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index 2246c64c..a193c26f 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1544,4 +1544,8 @@ class AppLocalizationsBn extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index c0d55c75..694aa2c7 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1554,4 +1554,8 @@ class AppLocalizationsCa extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 2c1e4dfa..8ef0e6a9 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1544,4 +1544,8 @@ class AppLocalizationsCs extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 12ce5e81..870dd76d 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1556,4 +1556,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index fdafac30..23d379c7 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1542,4 +1542,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index b9d2bb5a..f06c9399 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1557,4 +1557,8 @@ class AppLocalizationsEs extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 0ac13ddc..296c50ed 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1554,4 +1554,8 @@ class AppLocalizationsEu extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 140077c2..a1203c57 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1542,4 +1542,8 @@ class AppLocalizationsFa extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index b1d13d8e..b9ee4de6 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1542,4 +1542,8 @@ class AppLocalizationsFi extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 6a7e51cb..daee5667 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1562,4 +1562,8 @@ class AppLocalizationsFr extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 1be49d24..65279d70 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1548,4 +1548,8 @@ class AppLocalizationsHi extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 225fb9a5..1e0b9f9f 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1550,4 +1550,8 @@ class AppLocalizationsId extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 88128c6c..f92eae63 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1549,4 +1549,8 @@ class AppLocalizationsIt extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index f444e4e4..0e3d98ab 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1513,4 +1513,8 @@ class AppLocalizationsJa extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index a9efb949..22d3246f 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1551,4 +1551,8 @@ class AppLocalizationsKa extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index c6cc39c8..19b7e544 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1517,4 +1517,8 @@ class AppLocalizationsKo extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 6bd1214e..53bcb184 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1554,4 +1554,8 @@ class AppLocalizationsNe extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index e7d6ee40..dd73f907 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1548,4 +1548,8 @@ class AppLocalizationsNl extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index fe0493c0..095242bc 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1550,4 +1550,8 @@ class AppLocalizationsPl extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 082817ae..4b1fd2bc 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1547,4 +1547,8 @@ class AppLocalizationsPt extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 6645d9ff..9cd091e7 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1550,4 +1550,8 @@ class AppLocalizationsRu extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 98cff256..204832b2 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1556,4 +1556,8 @@ class AppLocalizationsTa extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 0156180e..5ea104a7 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1539,4 +1539,8 @@ class AppLocalizationsTh extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index ddecd3cf..de1916e5 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1557,4 +1557,8 @@ class AppLocalizationsTl extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 6e9e36f2..e6740bb3 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1550,4 +1550,8 @@ class AppLocalizationsTr extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 6d8e44b1..aba38ac3 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1546,4 +1546,8 @@ class AppLocalizationsUk extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index fcf545ae..ec449549 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1552,4 +1552,8 @@ class AppLocalizationsVi extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index ec8a4e4b..c9e18e72 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1506,6 +1506,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get uncompressed => 'Uncompressed'; + + @override + String get dab_music_source_description => + 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/models/connect/connect.freezed.dart b/lib/models/connect/connect.freezed.dart index 9f9b558b..157d0911 100644 --- a/lib/models/connect/connect.freezed.dart +++ b/lib/models/connect/connect.freezed.dart @@ -112,8 +112,13 @@ mixin _$WebSocketLoadEventData { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this WebSocketLoadEventData to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $WebSocketLoadEventDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -142,6 +147,8 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -190,6 +197,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res> $Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then) : super(_value, _then); + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -213,6 +222,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res> )); } + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection { @@ -281,12 +292,14 @@ class _$WebSocketLoadEventDataPlaylistImpl other.initialIndex == initialIndex)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_tracks), collection, initialIndex); - @JsonKey(ignore: true) + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WebSocketLoadEventDataPlaylistImplCopyWith< @@ -420,8 +433,11 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData { SpotubeSimplePlaylistObject? get collection; @override int? get initialIndex; + + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$WebSocketLoadEventDataPlaylistImplCopyWith< _$WebSocketLoadEventDataPlaylistImpl> get copyWith => throw _privateConstructorUsedError; @@ -456,6 +472,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res> $Res Function(_$WebSocketLoadEventDataAlbumImpl) _then) : super(_value, _then); + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -479,6 +497,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res> )); } + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection { @@ -545,12 +565,14 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum { other.initialIndex == initialIndex)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_tracks), collection, initialIndex); - @JsonKey(ignore: true) + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> @@ -683,8 +705,11 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData { SpotubeSimpleAlbumObject? get collection; @override int? get initialIndex; + + /// Create a copy of WebSocketLoadEventData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index ba24c037..8c4def7c 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -18,15 +18,12 @@ class $AuthenticationTableTable extends AuthenticationTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _cookieMeta = const VerificationMeta('cookie'); @override late final GeneratedColumnWithTypeConverter cookie = GeneratedColumn('cookie', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) .withConverter( $AuthenticationTableTable.$convertercookie); - static const VerificationMeta _accessTokenMeta = - const VerificationMeta('accessToken'); @override late final GeneratedColumnWithTypeConverter accessToken = GeneratedColumn('access_token', aliasedName, false, @@ -55,8 +52,6 @@ class $AuthenticationTableTable extends AuthenticationTable if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } - context.handle(_cookieMeta, const VerificationResult.success()); - context.handle(_accessTokenMeta, const VerificationResult.success()); if (data.containsKey('expiration')) { context.handle( _expirationMeta, @@ -301,8 +296,6 @@ class $BlacklistTableTable extends BlacklistTable late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _elementTypeMeta = - const VerificationMeta('elementType'); @override late final GeneratedColumnWithTypeConverter elementType = GeneratedColumn('element_type', aliasedName, false, @@ -336,7 +329,6 @@ class $BlacklistTableTable extends BlacklistTable } else if (isInserting) { context.missing(_nameMeta); } - context.handle(_elementTypeMeta, const VerificationResult.success()); if (data.containsKey('element_id')) { context.handle(_elementIdMeta, elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); @@ -566,8 +558,6 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - static const VerificationMeta _audioQualityMeta = - const VerificationMeta('audioQuality'); @override late final GeneratedColumnWithTypeConverter audioQuality = GeneratedColumn( @@ -647,8 +637,6 @@ class $PreferencesTableTable extends PreferencesTable defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("skip_non_music" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _closeBehaviorMeta = - const VerificationMeta('closeBehavior'); @override late final GeneratedColumnWithTypeConverter closeBehavior = GeneratedColumn( @@ -658,8 +646,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(CloseBehavior.close.name)) .withConverter( $PreferencesTableTable.$convertercloseBehavior); - static const VerificationMeta _accentColorSchemeMeta = - const VerificationMeta('accentColorScheme'); @override late final GeneratedColumnWithTypeConverter accentColorScheme = GeneratedColumn( @@ -669,8 +655,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: const Constant("Slate:0xff64748b")) .withConverter( $PreferencesTableTable.$converteraccentColorScheme); - static const VerificationMeta _layoutModeMeta = - const VerificationMeta('layoutMode'); @override late final GeneratedColumnWithTypeConverter layoutMode = GeneratedColumn('layout_mode', aliasedName, false, @@ -679,7 +663,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(LayoutMode.adaptive.name)) .withConverter( $PreferencesTableTable.$converterlayoutMode); - static const VerificationMeta _localeMeta = const VerificationMeta('locale'); @override late final GeneratedColumnWithTypeConverter locale = GeneratedColumn('locale', aliasedName, false, @@ -688,7 +671,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: const Constant( '{"languageCode":"system","countryCode":"system"}')) .withConverter($PreferencesTableTable.$converterlocale); - static const VerificationMeta _marketMeta = const VerificationMeta('market'); @override late final GeneratedColumnWithTypeConverter market = GeneratedColumn('market', aliasedName, false, @@ -696,8 +678,6 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultValue: Constant(Market.US.name)) .withConverter($PreferencesTableTable.$convertermarket); - static const VerificationMeta _searchModeMeta = - const VerificationMeta('searchMode'); @override late final GeneratedColumnWithTypeConverter searchMode = GeneratedColumn('search_mode', aliasedName, false, @@ -714,8 +694,6 @@ class $PreferencesTableTable extends PreferencesTable type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const Constant("")); - static const VerificationMeta _localLibraryLocationMeta = - const VerificationMeta('localLibraryLocation'); @override late final GeneratedColumnWithTypeConverter, String> localLibraryLocation = GeneratedColumn( @@ -741,8 +719,6 @@ class $PreferencesTableTable extends PreferencesTable type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const Constant("https://inv.nadeko.net")); - static const VerificationMeta _themeModeMeta = - const VerificationMeta('themeMode'); @override late final GeneratedColumnWithTypeConverter themeMode = GeneratedColumn('theme_mode', aliasedName, false, @@ -750,8 +726,6 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultValue: Constant(ThemeMode.system.name)) .withConverter($PreferencesTableTable.$converterthemeMode); - static const VerificationMeta _audioSourceMeta = - const VerificationMeta('audioSource'); @override late final GeneratedColumnWithTypeConverter audioSource = GeneratedColumn('audio_source', aliasedName, false, @@ -760,8 +734,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(AudioSource.youtube.name)) .withConverter( $PreferencesTableTable.$converteraudioSource); - static const VerificationMeta _youtubeClientEngineMeta = - const VerificationMeta('youtubeClientEngine'); @override late final GeneratedColumnWithTypeConverter youtubeClientEngine = GeneratedColumn( @@ -771,8 +743,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)) .withConverter( $PreferencesTableTable.$converteryoutubeClientEngine); - static const VerificationMeta _streamMusicCodecMeta = - const VerificationMeta('streamMusicCodec'); @override late final GeneratedColumnWithTypeConverter streamMusicCodec = GeneratedColumn( @@ -782,8 +752,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(SourceCodecs.weba.name)) .withConverter( $PreferencesTableTable.$converterstreamMusicCodec); - static const VerificationMeta _downloadMusicCodecMeta = - const VerificationMeta('downloadMusicCodec'); @override late final GeneratedColumnWithTypeConverter downloadMusicCodec = GeneratedColumn( @@ -887,7 +855,6 @@ class $PreferencesTableTable extends PreferencesTable if (data.containsKey('id')) { context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } - context.handle(_audioQualityMeta, const VerificationResult.success()); if (data.containsKey('album_color_sync')) { context.handle( _albumColorSyncMeta, @@ -930,20 +897,12 @@ class $PreferencesTableTable extends PreferencesTable skipNonMusic.isAcceptableOrUnknown( data['skip_non_music']!, _skipNonMusicMeta)); } - context.handle(_closeBehaviorMeta, const VerificationResult.success()); - context.handle(_accentColorSchemeMeta, const VerificationResult.success()); - context.handle(_layoutModeMeta, const VerificationResult.success()); - context.handle(_localeMeta, const VerificationResult.success()); - context.handle(_marketMeta, const VerificationResult.success()); - context.handle(_searchModeMeta, const VerificationResult.success()); if (data.containsKey('download_location')) { context.handle( _downloadLocationMeta, downloadLocation.isAcceptableOrUnknown( data['download_location']!, _downloadLocationMeta)); } - context.handle( - _localLibraryLocationMeta, const VerificationResult.success()); if (data.containsKey('piped_instance')) { context.handle( _pipedInstanceMeta, @@ -956,12 +915,6 @@ class $PreferencesTableTable extends PreferencesTable invidiousInstance.isAcceptableOrUnknown( data['invidious_instance']!, _invidiousInstanceMeta)); } - context.handle(_themeModeMeta, const VerificationResult.success()); - context.handle(_audioSourceMeta, const VerificationResult.success()); - context.handle( - _youtubeClientEngineMeta, const VerificationResult.success()); - context.handle(_streamMusicCodecMeta, const VerificationResult.success()); - context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); if (data.containsKey('discord_presence')) { context.handle( _discordPresenceMeta, @@ -2030,8 +1983,6 @@ class $ScrobblerTableTable extends ScrobblerTable late final GeneratedColumn username = GeneratedColumn( 'username', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _passwordHashMeta = - const VerificationMeta('passwordHash'); @override late final GeneratedColumnWithTypeConverter passwordHash = GeneratedColumn( @@ -2064,7 +2015,6 @@ class $ScrobblerTableTable extends ScrobblerTable } else if (isInserting) { context.missing(_usernameMeta); } - context.handle(_passwordHashMeta, const VerificationResult.success()); return context; } @@ -2595,8 +2545,6 @@ class $SourceMatchTableTable extends SourceMatchTable late final GeneratedColumn sourceId = GeneratedColumn( 'source_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _sourceTypeMeta = - const VerificationMeta('sourceType'); @override late final GeneratedColumnWithTypeConverter sourceType = GeneratedColumn('source_type', aliasedName, false, @@ -2642,7 +2590,6 @@ class $SourceMatchTableTable extends SourceMatchTable } else if (isInserting) { context.missing(_sourceIdMeta); } - context.handle(_sourceTypeMeta, const VerificationResult.success()); if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); @@ -2901,8 +2848,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); - static const VerificationMeta _loopModeMeta = - const VerificationMeta('loopMode'); @override late final GeneratedColumnWithTypeConverter loopMode = GeneratedColumn('loop_mode', aliasedName, false, @@ -2918,15 +2863,12 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); - static const VerificationMeta _collectionsMeta = - const VerificationMeta('collections'); @override late final GeneratedColumnWithTypeConverter, String> collections = GeneratedColumn('collections', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) .withConverter>( $AudioPlayerStateTableTable.$convertercollections); - static const VerificationMeta _tracksMeta = const VerificationMeta('tracks'); @override late final GeneratedColumnWithTypeConverter, String> tracks = GeneratedColumn('tracks', aliasedName, false, @@ -2966,15 +2908,12 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable } else if (isInserting) { context.missing(_playingMeta); } - context.handle(_loopModeMeta, const VerificationResult.success()); if (data.containsKey('shuffled')) { context.handle(_shuffledMeta, shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta)); } else if (isInserting) { context.missing(_shuffledMeta); } - context.handle(_collectionsMeta, const VerificationResult.success()); - context.handle(_tracksMeta, const VerificationResult.success()); if (data.containsKey('current_index')) { context.handle( _currentIndexMeta, @@ -3305,7 +3244,6 @@ class $HistoryTableTable extends HistoryTable type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override late final GeneratedColumnWithTypeConverter type = GeneratedColumn('type', aliasedName, false, @@ -3316,7 +3254,6 @@ class $HistoryTableTable extends HistoryTable late final GeneratedColumn itemId = GeneratedColumn( 'item_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _dataMeta = const VerificationMeta('data'); @override late final GeneratedColumnWithTypeConverter, String> data = GeneratedColumn('data', aliasedName, false, @@ -3342,14 +3279,12 @@ class $HistoryTableTable extends HistoryTable context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } - context.handle(_typeMeta, const VerificationResult.success()); if (data.containsKey('item_id')) { context.handle(_itemIdMeta, itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta)); } else if (isInserting) { context.missing(_itemIdMeta); } - context.handle(_dataMeta, const VerificationResult.success()); return context; } @@ -3608,7 +3543,6 @@ class $LyricsTableTable extends LyricsTable late final GeneratedColumn trackId = GeneratedColumn( 'track_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _dataMeta = const VerificationMeta('data'); @override late final GeneratedColumnWithTypeConverter data = GeneratedColumn('data', aliasedName, false, @@ -3635,7 +3569,6 @@ class $LyricsTableTable extends LyricsTable } else if (isInserting) { context.missing(_trackIdMeta); } - context.handle(_dataMeta, const VerificationResult.success()); return context; } @@ -3853,15 +3786,12 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable late final GeneratedColumn entryPoint = GeneratedColumn( 'entry_point', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _apisMeta = const VerificationMeta('apis'); @override late final GeneratedColumnWithTypeConverter, String> apis = GeneratedColumn('apis', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) .withConverter>( $MetadataPluginsTableTable.$converterapis); - static const VerificationMeta _abilitiesMeta = - const VerificationMeta('abilities'); @override late final GeneratedColumnWithTypeConverter, String> abilities = GeneratedColumn('abilities', aliasedName, false, @@ -3954,8 +3884,6 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable } else if (isInserting) { context.missing(_entryPointMeta); } - context.handle(_apisMeta, const VerificationResult.success()); - context.handle(_abilitiesMeta, const VerificationResult.success()); if (data.containsKey('selected')) { context.handle(_selectedMeta, selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta)); diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index 54fd452a..bb4cf3f8 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -33,8 +33,12 @@ mixin _$SpotubeFullAlbumObject { String? get recordLabel => throw _privateConstructorUsedError; List? get genres => throw _privateConstructorUsedError; + /// Serializes this SpotubeFullAlbumObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeFullAlbumObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeFullAlbumObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -69,6 +73,8 @@ class _$SpotubeFullAlbumObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeFullAlbumObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -160,6 +166,8 @@ class __$$SpotubeFullAlbumObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeFullAlbumObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeFullAlbumObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -308,7 +316,7 @@ class _$SpotubeFullAlbumObjectImpl implements _SpotubeFullAlbumObject { const DeepCollectionEquality().equals(other._genres, _genres)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -323,7 +331,9 @@ class _$SpotubeFullAlbumObjectImpl implements _SpotubeFullAlbumObject { recordLabel, const DeepCollectionEquality().hash(_genres)); - @JsonKey(ignore: true) + /// Create a copy of SpotubeFullAlbumObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeFullAlbumObjectImplCopyWith<_$SpotubeFullAlbumObjectImpl> @@ -374,8 +384,11 @@ abstract class _SpotubeFullAlbumObject implements SpotubeFullAlbumObject { String? get recordLabel; @override List? get genres; + + /// Create a copy of SpotubeFullAlbumObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeFullAlbumObjectImplCopyWith<_$SpotubeFullAlbumObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -396,8 +409,12 @@ mixin _$SpotubeSimpleAlbumObject { SpotubeAlbumType get albumType => throw _privateConstructorUsedError; String? get releaseDate => throw _privateConstructorUsedError; + /// Serializes this SpotubeSimpleAlbumObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeSimpleAlbumObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeSimpleAlbumObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -429,6 +446,8 @@ class _$SpotubeSimpleAlbumObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeSimpleAlbumObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -502,6 +521,8 @@ class __$$SpotubeSimpleAlbumObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeSimpleAlbumObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeSimpleAlbumObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -613,7 +634,7 @@ class _$SpotubeSimpleAlbumObjectImpl implements _SpotubeSimpleAlbumObject { other.releaseDate == releaseDate)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -625,7 +646,9 @@ class _$SpotubeSimpleAlbumObjectImpl implements _SpotubeSimpleAlbumObject { albumType, releaseDate); - @JsonKey(ignore: true) + /// Create a copy of SpotubeSimpleAlbumObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeSimpleAlbumObjectImplCopyWith<_$SpotubeSimpleAlbumObjectImpl> @@ -667,8 +690,11 @@ abstract class _SpotubeSimpleAlbumObject implements SpotubeSimpleAlbumObject { SpotubeAlbumType get albumType; @override String? get releaseDate; + + /// Create a copy of SpotubeSimpleAlbumObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeSimpleAlbumObjectImplCopyWith<_$SpotubeSimpleAlbumObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -687,8 +713,12 @@ mixin _$SpotubeFullArtistObject { List? get genres => throw _privateConstructorUsedError; int? get followers => throw _privateConstructorUsedError; + /// Serializes this SpotubeFullArtistObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeFullArtistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeFullArtistObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -719,6 +749,8 @@ class _$SpotubeFullArtistObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeFullArtistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -786,6 +818,8 @@ class __$$SpotubeFullArtistObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeFullArtistObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeFullArtistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -889,7 +923,7 @@ class _$SpotubeFullArtistObjectImpl implements _SpotubeFullArtistObject { other.followers == followers)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -900,7 +934,9 @@ class _$SpotubeFullArtistObjectImpl implements _SpotubeFullArtistObject { const DeepCollectionEquality().hash(_genres), followers); - @JsonKey(ignore: true) + /// Create a copy of SpotubeFullArtistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeFullArtistObjectImplCopyWith<_$SpotubeFullArtistObjectImpl> @@ -939,8 +975,11 @@ abstract class _SpotubeFullArtistObject implements SpotubeFullArtistObject { List? get genres; @override int? get followers; + + /// Create a copy of SpotubeFullArtistObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeFullArtistObjectImplCopyWith<_$SpotubeFullArtistObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -957,8 +996,12 @@ mixin _$SpotubeSimpleArtistObject { String get externalUri => throw _privateConstructorUsedError; List? get images => throw _privateConstructorUsedError; + /// Serializes this SpotubeSimpleArtistObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeSimpleArtistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeSimpleArtistObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -987,6 +1030,8 @@ class _$SpotubeSimpleArtistObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeSimpleArtistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1042,6 +1087,8 @@ class __$$SpotubeSimpleArtistObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeSimpleArtistObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeSimpleArtistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1117,12 +1164,14 @@ class _$SpotubeSimpleArtistObjectImpl implements _SpotubeSimpleArtistObject { const DeepCollectionEquality().equals(other._images, _images)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, name, externalUri, const DeepCollectionEquality().hash(_images)); - @JsonKey(ignore: true) + /// Create a copy of SpotubeSimpleArtistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeSimpleArtistObjectImplCopyWith<_$SpotubeSimpleArtistObjectImpl> @@ -1156,8 +1205,11 @@ abstract class _SpotubeSimpleArtistObject implements SpotubeSimpleArtistObject { String get externalUri; @override List? get images; + + /// Create a copy of SpotubeSimpleArtistObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeSimpleArtistObjectImplCopyWith<_$SpotubeSimpleArtistObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1175,9 +1227,13 @@ mixin _$SpotubeBrowseSectionObject { bool get browseMore => throw _privateConstructorUsedError; List get items => throw _privateConstructorUsedError; + /// Serializes this SpotubeBrowseSectionObject to a JSON map. Map toJson(Object? Function(T) toJsonT) => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeBrowseSectionObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeBrowseSectionObjectCopyWith> get copyWith => throw _privateConstructorUsedError; } @@ -1209,6 +1265,8 @@ class _$SpotubeBrowseSectionObjectCopyWithImpl $Res Function(_$SpotubeBrowseSectionObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeBrowseSectionObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1355,12 +1415,14 @@ class _$SpotubeBrowseSectionObjectImpl const DeepCollectionEquality().equals(other._items, _items)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, title, externalUri, browseMore, const DeepCollectionEquality().hash(_items)); - @JsonKey(ignore: true) + /// Create a copy of SpotubeBrowseSectionObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeBrowseSectionObjectImplCopyWith bool get browseMore; @override List get items; + + /// Create a copy of SpotubeBrowseSectionObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeBrowseSectionObjectImplCopyWith> get copyWith => throw _privateConstructorUsedError; @@ -1486,8 +1551,13 @@ mixin _$MetadataFormFieldObject { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this MetadataFormFieldObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $MetadataFormFieldObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -1512,6 +1582,8 @@ class _$MetadataFormFieldObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1555,6 +1627,8 @@ class __$$MetadataFormFieldInputObjectImplCopyWithImpl<$Res> $Res Function(_$MetadataFormFieldInputObjectImpl) _then) : super(_value, _then); + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1655,12 +1729,14 @@ class _$MetadataFormFieldInputObjectImpl (identical(other.regex, regex) || other.regex == regex)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, objectType, id, variant, placeholder, defaultValue, required, regex); - @JsonKey(ignore: true) + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MetadataFormFieldInputObjectImplCopyWith< @@ -1786,8 +1862,11 @@ abstract class MetadataFormFieldInputObject implements MetadataFormFieldObject { String? get defaultValue; bool? get required; String? get regex; + + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$MetadataFormFieldInputObjectImplCopyWith< _$MetadataFormFieldInputObjectImpl> get copyWith => throw _privateConstructorUsedError; @@ -1815,6 +1894,8 @@ class __$$MetadataFormFieldTextObjectImplCopyWithImpl<$Res> $Res Function(_$MetadataFormFieldTextObjectImpl) _then) : super(_value, _then); + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1864,11 +1945,13 @@ class _$MetadataFormFieldTextObjectImpl implements MetadataFormFieldTextObject { (identical(other.text, text) || other.text == text)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, objectType, text); - @JsonKey(ignore: true) + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MetadataFormFieldTextObjectImplCopyWith<_$MetadataFormFieldTextObjectImpl> @@ -1980,8 +2063,11 @@ abstract class MetadataFormFieldTextObject implements MetadataFormFieldObject { @override String get objectType; String get text; + + /// Create a copy of MetadataFormFieldObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$MetadataFormFieldTextObjectImplCopyWith<_$MetadataFormFieldTextObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1996,8 +2082,12 @@ mixin _$SpotubeImageObject { int? get width => throw _privateConstructorUsedError; int? get height => throw _privateConstructorUsedError; + /// Serializes this SpotubeImageObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeImageObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeImageObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -2021,6 +2111,8 @@ class _$SpotubeImageObjectCopyWithImpl<$Res, $Val extends SpotubeImageObject> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeImageObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2064,6 +2156,8 @@ class __$$SpotubeImageObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeImageObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeImageObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2118,11 +2212,13 @@ class _$SpotubeImageObjectImpl implements _SpotubeImageObject { (identical(other.height, height) || other.height == height)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, url, width, height); - @JsonKey(ignore: true) + /// Create a copy of SpotubeImageObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeImageObjectImplCopyWith<_$SpotubeImageObjectImpl> get copyWith => @@ -2152,8 +2248,11 @@ abstract class _SpotubeImageObject implements SpotubeImageObject { int? get width; @override int? get height; + + /// Create a copy of SpotubeImageObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeImageObjectImplCopyWith<_$SpotubeImageObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2171,9 +2270,13 @@ mixin _$SpotubePaginationResponseObject { bool get hasMore => throw _privateConstructorUsedError; List get items => throw _privateConstructorUsedError; + /// Serializes this SpotubePaginationResponseObject to a JSON map. Map toJson(Object? Function(T) toJsonT) => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubePaginationResponseObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubePaginationResponseObjectCopyWith> get copyWith => throw _privateConstructorUsedError; @@ -2202,6 +2305,8 @@ class _$SpotubePaginationResponseObjectCopyWithImpl $Res Function(_$SpotubePaginationResponseObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubePaginationResponseObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2343,12 +2450,14 @@ class _$SpotubePaginationResponseObjectImpl const DeepCollectionEquality().equals(other._items, _items)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, limit, nextOffset, total, hasMore, const DeepCollectionEquality().hash(_items)); - @JsonKey(ignore: true) + /// Create a copy of SpotubePaginationResponseObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubePaginationResponseObjectImplCopyWith bool get hasMore; @override List get items; + + /// Create a copy of SpotubePaginationResponseObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubePaginationResponseObjectImplCopyWith> get copyWith => throw _privateConstructorUsedError; @@ -2410,8 +2522,12 @@ mixin _$SpotubeFullPlaylistObject { bool get collaborative => throw _privateConstructorUsedError; bool get public => throw _privateConstructorUsedError; + /// Serializes this SpotubeFullPlaylistObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeFullPlaylistObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -2447,6 +2563,8 @@ class _$SpotubeFullPlaylistObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2500,6 +2618,8 @@ class _$SpotubeFullPlaylistObjectCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SpotubeUserObjectCopyWith<$Res> get owner { @@ -2543,6 +2663,8 @@ class __$$SpotubeFullPlaylistObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeFullPlaylistObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2676,7 +2798,7 @@ class _$SpotubeFullPlaylistObjectImpl implements _SpotubeFullPlaylistObject { (identical(other.public, public) || other.public == public)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -2690,7 +2812,9 @@ class _$SpotubeFullPlaylistObjectImpl implements _SpotubeFullPlaylistObject { collaborative, public); - @JsonKey(ignore: true) + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeFullPlaylistObjectImplCopyWith<_$SpotubeFullPlaylistObjectImpl> @@ -2738,8 +2862,11 @@ abstract class _SpotubeFullPlaylistObject implements SpotubeFullPlaylistObject { bool get collaborative; @override bool get public; + + /// Create a copy of SpotubeFullPlaylistObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeFullPlaylistObjectImplCopyWith<_$SpotubeFullPlaylistObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -2758,8 +2885,12 @@ mixin _$SpotubeSimplePlaylistObject { SpotubeUserObject get owner => throw _privateConstructorUsedError; List get images => throw _privateConstructorUsedError; + /// Serializes this SpotubeSimplePlaylistObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeSimplePlaylistObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -2794,6 +2925,8 @@ class _$SpotubeSimplePlaylistObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2832,6 +2965,8 @@ class _$SpotubeSimplePlaylistObjectCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SpotubeUserObjectCopyWith<$Res> get owner { @@ -2872,6 +3007,8 @@ class __$$SpotubeSimplePlaylistObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeSimplePlaylistObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -2967,12 +3104,14 @@ class _$SpotubeSimplePlaylistObjectImpl const DeepCollectionEquality().equals(other._images, _images)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, name, description, externalUri, owner, const DeepCollectionEquality().hash(_images)); - @JsonKey(ignore: true) + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeSimplePlaylistObjectImplCopyWith<_$SpotubeSimplePlaylistObjectImpl> @@ -3013,8 +3152,11 @@ abstract class _SpotubeSimplePlaylistObject SpotubeUserObject get owner; @override List get images; + + /// Create a copy of SpotubeSimplePlaylistObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeSimplePlaylistObjectImplCopyWith<_$SpotubeSimplePlaylistObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -3034,8 +3176,12 @@ mixin _$SpotubeSearchResponseObject { throw _privateConstructorUsedError; List get tracks => throw _privateConstructorUsedError; + /// Serializes this SpotubeSearchResponseObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeSearchResponseObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeSearchResponseObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -3066,6 +3212,8 @@ class _$SpotubeSearchResponseObjectCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeSearchResponseObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3121,6 +3269,8 @@ class __$$SpotubeSearchResponseObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeSearchResponseObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeSearchResponseObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3217,7 +3367,7 @@ class _$SpotubeSearchResponseObjectImpl const DeepCollectionEquality().equals(other._tracks, _tracks)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -3226,7 +3376,9 @@ class _$SpotubeSearchResponseObjectImpl const DeepCollectionEquality().hash(_playlists), const DeepCollectionEquality().hash(_tracks)); - @JsonKey(ignore: true) + /// Create a copy of SpotubeSearchResponseObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeSearchResponseObjectImplCopyWith<_$SpotubeSearchResponseObjectImpl> @@ -3261,8 +3413,11 @@ abstract class _SpotubeSearchResponseObject List get playlists; @override List get tracks; + + /// Create a copy of SpotubeSearchResponseObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeSearchResponseObjectImplCopyWith<_$SpotubeSearchResponseObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -3378,8 +3533,13 @@ mixin _$SpotubeTrackObject { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this SpotubeTrackObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeTrackObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -3411,6 +3571,8 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3449,6 +3611,8 @@ class _$SpotubeTrackObjectCopyWithImpl<$Res, $Val extends SpotubeTrackObject> ) as $Val); } + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SpotubeSimpleAlbumObjectCopyWith<$Res> get album { @@ -3490,6 +3654,8 @@ class __$$SpotubeLocalTrackObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeLocalTrackObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3598,12 +3764,14 @@ class _$SpotubeLocalTrackObjectImpl implements SpotubeLocalTrackObject { (identical(other.path, path) || other.path == path)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, name, externalUri, const DeepCollectionEquality().hash(_artists), album, durationMs, path); - @JsonKey(ignore: true) + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeLocalTrackObjectImplCopyWith<_$SpotubeLocalTrackObjectImpl> @@ -3757,8 +3925,11 @@ abstract class SpotubeLocalTrackObject implements SpotubeTrackObject { @override int get durationMs; String get path; + + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeLocalTrackObjectImplCopyWith<_$SpotubeLocalTrackObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -3795,6 +3966,8 @@ class __$$SpotubeFullTrackObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeFullTrackObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -3913,7 +4086,7 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { other.explicit == explicit)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -3926,7 +4099,9 @@ class _$SpotubeFullTrackObjectImpl implements SpotubeFullTrackObject { isrc, explicit); - @JsonKey(ignore: true) + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeFullTrackObjectImplCopyWith<_$SpotubeFullTrackObjectImpl> @@ -4085,8 +4260,11 @@ abstract class SpotubeFullTrackObject implements SpotubeTrackObject { int get durationMs; String get isrc; bool get explicit; + + /// Create a copy of SpotubeTrackObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeFullTrackObjectImplCopyWith<_$SpotubeFullTrackObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -4102,8 +4280,12 @@ mixin _$SpotubeUserObject { List get images => throw _privateConstructorUsedError; String get externalUri => throw _privateConstructorUsedError; + /// Serializes this SpotubeUserObject to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SpotubeUserObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SpotubeUserObjectCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -4131,6 +4313,8 @@ class _$SpotubeUserObjectCopyWithImpl<$Res, $Val extends SpotubeUserObject> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SpotubeUserObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4183,6 +4367,8 @@ class __$$SpotubeUserObjectImplCopyWithImpl<$Res> $Res Function(_$SpotubeUserObjectImpl) _then) : super(_value, _then); + /// Create a copy of SpotubeUserObject + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4258,12 +4444,14 @@ class _$SpotubeUserObjectImpl implements _SpotubeUserObject { other.externalUri == externalUri)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, id, name, const DeepCollectionEquality().hash(_images), externalUri); - @JsonKey(ignore: true) + /// Create a copy of SpotubeUserObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SpotubeUserObjectImplCopyWith<_$SpotubeUserObjectImpl> get copyWith => @@ -4296,8 +4484,11 @@ abstract class _SpotubeUserObject implements SpotubeUserObject { List get images; @override String get externalUri; + + /// Create a copy of SpotubeUserObject + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SpotubeUserObjectImplCopyWith<_$SpotubeUserObjectImpl> get copyWith => throw _privateConstructorUsedError; } @@ -4319,8 +4510,12 @@ mixin _$PluginConfiguration { List get abilities => throw _privateConstructorUsedError; String? get repository => throw _privateConstructorUsedError; + /// Serializes this PluginConfiguration to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PluginConfigurationCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -4354,6 +4549,8 @@ class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4442,6 +4639,8 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> $Res Function(_$PluginConfigurationImpl) _then) : super(_value, _then); + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4584,7 +4783,7 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { other.repository == repository)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -4599,7 +4798,9 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { const DeepCollectionEquality().hash(_abilities), repository); - @JsonKey(ignore: true) + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith => @@ -4651,8 +4852,11 @@ abstract class _PluginConfiguration extends PluginConfiguration { List get abilities; @override String? get repository; + + /// Create a copy of PluginConfiguration + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PluginConfigurationImplCopyWith<_$PluginConfigurationImpl> get copyWith => throw _privateConstructorUsedError; } @@ -4668,8 +4872,12 @@ mixin _$PluginUpdateAvailable { String get version => throw _privateConstructorUsedError; String? get changelog => throw _privateConstructorUsedError; + /// Serializes this PluginUpdateAvailable to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of PluginUpdateAvailable + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PluginUpdateAvailableCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -4694,6 +4902,8 @@ class _$PluginUpdateAvailableCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PluginUpdateAvailable + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4739,6 +4949,8 @@ class __$$PluginUpdateAvailableImplCopyWithImpl<$Res> $Res Function(_$PluginUpdateAvailableImpl) _then) : super(_value, _then); + /// Create a copy of PluginUpdateAvailable + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4796,11 +5008,13 @@ class _$PluginUpdateAvailableImpl implements _PluginUpdateAvailable { other.changelog == changelog)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, downloadUrl, version, changelog); - @JsonKey(ignore: true) + /// Create a copy of PluginUpdateAvailable + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PluginUpdateAvailableImplCopyWith<_$PluginUpdateAvailableImpl> @@ -4830,8 +5044,11 @@ abstract class _PluginUpdateAvailable implements PluginUpdateAvailable { String get version; @override String? get changelog; + + /// Create a copy of PluginUpdateAvailable + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PluginUpdateAvailableImplCopyWith<_$PluginUpdateAvailableImpl> get copyWith => throw _privateConstructorUsedError; } @@ -4848,8 +5065,12 @@ mixin _$MetadataPluginRepository { String get description => throw _privateConstructorUsedError; String get repoUrl => throw _privateConstructorUsedError; + /// Serializes this MetadataPluginRepository to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of MetadataPluginRepository + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $MetadataPluginRepositoryCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -4874,6 +5095,8 @@ class _$MetadataPluginRepositoryCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of MetadataPluginRepository + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4925,6 +5148,8 @@ class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res> $Res Function(_$MetadataPluginRepositoryImpl) _then) : super(_value, _then); + /// Create a copy of MetadataPluginRepository + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -4992,12 +5217,14 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { (identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, name, owner, description, repoUrl); - @JsonKey(ignore: true) + /// Create a copy of MetadataPluginRepository + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MetadataPluginRepositoryImplCopyWith<_$MetadataPluginRepositoryImpl> @@ -5030,8 +5257,11 @@ abstract class _MetadataPluginRepository implements MetadataPluginRepository { String get description; @override String get repoUrl; + + /// Create a copy of MetadataPluginRepository + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$MetadataPluginRepositoryImplCopyWith<_$MetadataPluginRepositoryImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index c9d089a6..1666609c 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -103,6 +103,7 @@ class TrackSource with _$TrackSource { required SourceQualities quality, required SourceCodecs codec, required String bitrate, + required String qualityLabel, }) = _TrackSource; factory TrackSource.fromJson(Map json) => diff --git a/lib/models/playback/track_sources.freezed.dart b/lib/models/playback/track_sources.freezed.dart index 760037d8..09ceb399 100644 --- a/lib/models/playback/track_sources.freezed.dart +++ b/lib/models/playback/track_sources.freezed.dart @@ -28,8 +28,12 @@ mixin _$TrackSourceQuery { String get isrc => throw _privateConstructorUsedError; bool get explicit => throw _privateConstructorUsedError; + /// Serializes this TrackSourceQuery to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of TrackSourceQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $TrackSourceQueryCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -60,6 +64,8 @@ class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of TrackSourceQuery + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -130,6 +136,8 @@ class __$$TrackSourceQueryImplCopyWithImpl<$Res> $Res Function(_$TrackSourceQueryImpl) _then) : super(_value, _then); + /// Create a copy of TrackSourceQuery + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -233,7 +241,7 @@ class _$TrackSourceQueryImpl extends _TrackSourceQuery { other.explicit == explicit)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -245,7 +253,9 @@ class _$TrackSourceQueryImpl extends _TrackSourceQuery { isrc, explicit); - @JsonKey(ignore: true) + /// Create a copy of TrackSourceQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => @@ -288,8 +298,11 @@ abstract class _TrackSourceQuery extends TrackSourceQuery { String get isrc; @override bool get explicit; + + /// Create a copy of TrackSourceQuery + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => throw _privateConstructorUsedError; } @@ -307,8 +320,12 @@ mixin _$TrackSourceInfo { String get pageUrl => throw _privateConstructorUsedError; int get durationMs => throw _privateConstructorUsedError; + /// Serializes this TrackSourceInfo to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of TrackSourceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $TrackSourceInfoCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -338,6 +355,8 @@ class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of TrackSourceInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -402,6 +421,8 @@ class __$$TrackSourceInfoImplCopyWithImpl<$Res> _$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then) : super(_value, _then); + /// Create a copy of TrackSourceInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -488,12 +509,14 @@ class _$TrackSourceInfoImpl implements _TrackSourceInfo { other.durationMs == durationMs)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, id, title, artists, thumbnail, pageUrl, durationMs); - @JsonKey(ignore: true) + /// Create a copy of TrackSourceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => @@ -532,8 +555,11 @@ abstract class _TrackSourceInfo implements TrackSourceInfo { String get pageUrl; @override int get durationMs; + + /// Create a copy of TrackSourceInfo + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => throw _privateConstructorUsedError; } @@ -548,9 +574,14 @@ mixin _$TrackSource { SourceQualities get quality => throw _privateConstructorUsedError; SourceCodecs get codec => throw _privateConstructorUsedError; String get bitrate => throw _privateConstructorUsedError; + String get qualityLabel => throw _privateConstructorUsedError; + /// Serializes this TrackSource to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of TrackSource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $TrackSourceCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -565,7 +596,8 @@ abstract class $TrackSourceCopyWith<$Res> { {String url, SourceQualities quality, SourceCodecs codec, - String bitrate}); + String bitrate, + String qualityLabel}); } /// @nodoc @@ -578,6 +610,8 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of TrackSource + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -585,6 +619,7 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource> Object? quality = null, Object? codec = null, Object? bitrate = null, + Object? qualityLabel = null, }) { return _then(_value.copyWith( url: null == url @@ -603,6 +638,10 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource> ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable as String, + qualityLabel: null == qualityLabel + ? _value.qualityLabel + : qualityLabel // ignore: cast_nullable_to_non_nullable + as String, ) as $Val); } } @@ -619,7 +658,8 @@ abstract class _$$TrackSourceImplCopyWith<$Res> {String url, SourceQualities quality, SourceCodecs codec, - String bitrate}); + String bitrate, + String qualityLabel}); } /// @nodoc @@ -630,6 +670,8 @@ class __$$TrackSourceImplCopyWithImpl<$Res> _$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then) : super(_value, _then); + /// Create a copy of TrackSource + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -637,6 +679,7 @@ class __$$TrackSourceImplCopyWithImpl<$Res> Object? quality = null, Object? codec = null, Object? bitrate = null, + Object? qualityLabel = null, }) { return _then(_$TrackSourceImpl( url: null == url @@ -655,6 +698,10 @@ class __$$TrackSourceImplCopyWithImpl<$Res> ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable as String, + qualityLabel: null == qualityLabel + ? _value.qualityLabel + : qualityLabel // ignore: cast_nullable_to_non_nullable + as String, )); } } @@ -666,7 +713,8 @@ class _$TrackSourceImpl implements _TrackSource { {required this.url, required this.quality, required this.codec, - required this.bitrate}); + required this.bitrate, + required this.qualityLabel}); factory _$TrackSourceImpl.fromJson(Map json) => _$$TrackSourceImplFromJson(json); @@ -679,10 +727,12 @@ class _$TrackSourceImpl implements _TrackSource { final SourceCodecs codec; @override final String bitrate; + @override + final String qualityLabel; @override String toString() { - return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate)'; + return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)'; } @override @@ -693,14 +743,19 @@ class _$TrackSourceImpl implements _TrackSource { (identical(other.url, url) || other.url == url) && (identical(other.quality, quality) || other.quality == quality) && (identical(other.codec, codec) || other.codec == codec) && - (identical(other.bitrate, bitrate) || other.bitrate == bitrate)); + (identical(other.bitrate, bitrate) || other.bitrate == bitrate) && + (identical(other.qualityLabel, qualityLabel) || + other.qualityLabel == qualityLabel)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, url, quality, codec, bitrate); + int get hashCode => + Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel); - @JsonKey(ignore: true) + /// Create a copy of TrackSource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => @@ -719,7 +774,8 @@ abstract class _TrackSource implements TrackSource { {required final String url, required final SourceQualities quality, required final SourceCodecs codec, - required final String bitrate}) = _$TrackSourceImpl; + required final String bitrate, + required final String qualityLabel}) = _$TrackSourceImpl; factory _TrackSource.fromJson(Map json) = _$TrackSourceImpl.fromJson; @@ -733,7 +789,12 @@ abstract class _TrackSource implements TrackSource { @override String get bitrate; @override - @JsonKey(ignore: true) + String get qualityLabel; + + /// Create a copy of TrackSource + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/playback/track_sources.g.dart b/lib/models/playback/track_sources.g.dart index 4676490d..dd63aebb 100644 --- a/lib/models/playback/track_sources.g.dart +++ b/lib/models/playback/track_sources.g.dart @@ -36,6 +36,7 @@ const _$AudioSourceEnumMap = { AudioSource.piped: 'piped', AudioSource.jiosaavn: 'jiosaavn', AudioSource.invidious: 'invidious', + AudioSource.dabMusic: 'dabMusic', }; _$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) => @@ -88,6 +89,7 @@ _$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl( quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']), codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']), bitrate: json['bitrate'] as String, + qualityLabel: json['qualityLabel'] as String, ); Map _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => @@ -96,9 +98,11 @@ Map _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => 'quality': _$SourceQualitiesEnumMap[instance.quality]!, 'codec': _$SourceCodecsEnumMap[instance.codec]!, 'bitrate': instance.bitrate, + 'qualityLabel': instance.qualityLabel, }; const _$SourceQualitiesEnumMap = { + SourceQualities.uncompressed: 'uncompressed', SourceQualities.high: 'high', SourceQualities.medium: 'medium', SourceQualities.low: 'low', @@ -107,4 +111,6 @@ const _$SourceQualitiesEnumMap = { const _$SourceCodecsEnumMap = { SourceCodecs.m4a: 'm4a', SourceCodecs.weba: 'weba', + SourceCodecs.mp3: 'mp3', + SourceCodecs.flac: 'flac', }; diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 4d050afc..ea8cbf29 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -91,10 +91,27 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ) else ...[ Text(context.l10n.author_name(plugin.author)), - DestructiveBadge( - leading: const Icon(SpotubeIcons.warning), - child: Text(context.l10n.third_party), - ) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + const Icon(SpotubeIcons.warning, size: 14), + Text( + context.l10n.third_party, + style: const TextStyle(color: Colors.white), + ).xSmall + ], + ), + ), ], SecondaryBadge( leading: const Icon(SpotubeIcons.connect), diff --git a/lib/modules/metadata_plugins/plugin_repository.dart b/lib/modules/metadata_plugins/plugin_repository.dart index 295aed53..c303c46b 100644 --- a/lib/modules/metadata_plugins/plugin_repository.dart +++ b/lib/modules/metadata_plugins/plugin_repository.dart @@ -65,8 +65,9 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget { .join("\n\n"); return AlertDialog( - title: Text(context - .l10n.do_you_want_to_install_this_plugin), + title: Text( + context.l10n.do_you_want_to_install_this_plugin, + ), content: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -185,9 +186,26 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget { context.l10n.author_name(pluginRepo.owner), style: context.theme.typography.xSmall, ), - DestructiveBadge( - leading: const Icon(SpotubeIcons.warning), - child: Text(context.l10n.third_party), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + const Icon(SpotubeIcons.warning, size: 14), + Text( + context.l10n.third_party, + style: const TextStyle(color: Colors.white), + ).xSmall + ], + ), ), ], SecondaryBadge( diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index ec903aab..4250e153 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -46,6 +46,14 @@ class PlayerView extends HookConsumerWidget { final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final mediaQuery = MediaQuery.sizeOf(context); + final activeSourceCodec = useMemoized( + () { + return currentActiveTrackSource + ?.getSourceOfCodec(currentActiveTrackSource.codec); + }, + [currentActiveTrackSource?.sources, currentActiveTrackSource?.codec], + ); + final shouldHide = useState(true); ref.listen(navigationPanelHeight, (_, height) { @@ -267,6 +275,21 @@ class PlayerView extends HookConsumerWidget { ); }), ), + const Gap(25), + if (activeSourceCodec != null) + OutlineBadge( + style: const ButtonStyle.outline( + size: ButtonSize.normal, + density: ButtonDensity.dense, + shape: ButtonShape.rectangle, + ).copyWith( + textStyle: (context, states, value) { + return value.copyWith(fontWeight: FontWeight.w500); + }, + ), + leading: const Icon(SpotubeIcons.lightningOutlined), + child: Text(activeSourceCodec.qualityLabel), + ) ], ), ), diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index 43ff2c8e..6e1a29f4 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -7,7 +7,6 @@ import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/extensions/string.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; final audioSourceToIconMap = { @@ -23,6 +22,8 @@ final audioSourceToIconMap = { ), AudioSource.jiosaavn: Assets.images.logos.jiosaavn.image(width: 20, height: 20), + AudioSource.dabMusic: + Assets.images.logos.dabMusic.image(width: 20, height: 20), }; class GettingStartedPagePlaybackSection extends HookConsumerWidget { @@ -47,8 +48,10 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { AudioSource.piped: context.l10n.piped_source_description, AudioSource.jiosaavn: "${context.l10n.jiosaavn_source_description}\n" - "${context.l10n.highest_quality("320kbps mp")}", + "${context.l10n.highest_quality("320kbps mp4")}", AudioSource.invidious: context.l10n.invidious_source_description, + AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" + "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", }, []); @@ -70,43 +73,28 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { child: Text(context.l10n.select_audio_source).semiBold().large(), ), const Gap(16), - Select( + RadioGroup( value: preferences.audioSource, onChanged: (value) { - if (value == null) return; preferencesNotifier.setAudioSource(value); }, - placeholder: Text(preferences.audioSource.name.capitalize()), - itemBuilder: (context, value) => Row( - mainAxisSize: MainAxisSize.min, + child: Wrap( spacing: 6, + runSpacing: 6, children: [ - audioSourceToIconMap[value]!, - Text(value.name.capitalize()), + for (final source in AudioSource.values) + RadioCard( + value: source, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + audioSourceToIconMap[source]!, + Text(source.label), + ], + ), + ), ], ), - popup: (context) { - return SelectPopup( - items: SelectItemBuilder( - childCount: AudioSource.values.length, - builder: (context, index) { - final source = AudioSource.values[index]; - - return SelectItemButton( - value: source, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 6, - children: [ - audioSourceToIconMap[source]!, - Text(source.name.capitalize()), - ], - ), - ); - }, - ), - ); - }, ), const Gap(16), Text( diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 2d128742..6d0b5dc3 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -53,14 +53,16 @@ class SettingsPlaybackSection extends HookConsumerWidget { value: SourceQualities.high, child: Text(context.l10n.high), ), - SelectItemButton( - value: SourceQualities.medium, - child: Text(context.l10n.medium), - ), - SelectItemButton( - value: SourceQualities.low, - child: Text(context.l10n.low), - ), + if (preferences.audioSource != AudioSource.dabMusic) ...[ + SelectItemButton( + value: SourceQualities.medium, + child: Text(context.l10n.medium), + ), + SelectItemButton( + value: SourceQualities.low, + child: Text(context.l10n.low), + ), + ] ], onChanged: (value) { if (value != null) { diff --git a/lib/provider/audio_player/state.freezed.dart b/lib/provider/audio_player/state.freezed.dart index 146b0541..0299cd2f 100644 --- a/lib/provider/audio_player/state.freezed.dart +++ b/lib/provider/audio_player/state.freezed.dart @@ -27,8 +27,12 @@ mixin _$AudioPlayerState { int get currentIndex => throw _privateConstructorUsedError; List get tracks => throw _privateConstructorUsedError; + /// Serializes this AudioPlayerState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of AudioPlayerState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AudioPlayerStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -58,6 +62,8 @@ class _$AudioPlayerStateCopyWithImpl<$Res, $Val extends AudioPlayerState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of AudioPlayerState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -122,6 +128,8 @@ class __$$AudioPlayerStateImplCopyWithImpl<$Res> $Res Function(_$AudioPlayerStateImpl) _then) : super(_value, _then); + /// Create a copy of AudioPlayerState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -226,7 +234,7 @@ class _$AudioPlayerStateImpl extends _AudioPlayerState { const DeepCollectionEquality().equals(other._tracks, _tracks)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -237,7 +245,9 @@ class _$AudioPlayerStateImpl extends _AudioPlayerState { currentIndex, const DeepCollectionEquality().hash(_tracks)); - @JsonKey(ignore: true) + /// Create a copy of AudioPlayerState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith => @@ -277,8 +287,11 @@ abstract class _AudioPlayerState extends AudioPlayerState { int get currentIndex; @override List get tracks; + + /// Create a copy of AudioPlayerState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 8e8da561..8e72727c 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -224,11 +224,23 @@ class UserPreferencesNotifier extends Notifier { } void setAudioSource(AudioSource type) { - // Only allow uncompressed quality for DAB Music - if (type != AudioSource.dabMusic && - state.audioQuality == SourceQualities.uncompressed) { - setAudioQuality(SourceQualities.high); + switch ((type, state.audioQuality)) { + // DAB music only supports high quality/uncompressed streams + case ( + AudioSource.dabMusic, + SourceQualities.low || SourceQualities.medium + ): + setAudioQuality(SourceQualities.high); + break; + // If the user switches from DAB music to other sources and has + // uncompressed quality selected, downgrade to high quality + case (!= AudioSource.dabMusic, SourceQualities.uncompressed): + setAudioQuality(SourceQualities.high); + break; + default: + break; } + setData(PreferencesTableCompanion(audioSource: Value(type))); } diff --git a/lib/services/song_link/song_link.freezed.dart b/lib/services/song_link/song_link.freezed.dart index 0a1af8a9..c704cde3 100644 --- a/lib/services/song_link/song_link.freezed.dart +++ b/lib/services/song_link/song_link.freezed.dart @@ -30,8 +30,12 @@ mixin _$SongLink { String? get nativeAppUriMobile => throw _privateConstructorUsedError; String? get nativeAppUriDesktop => throw _privateConstructorUsedError; + /// Serializes this SongLink to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SongLink + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SongLinkCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -63,6 +67,8 @@ class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SongLink + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -145,6 +151,8 @@ class __$$SongLinkImplCopyWithImpl<$Res> _$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then) : super(_value, _then); + /// Create a copy of SongLink + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -261,12 +269,14 @@ class _$SongLinkImpl implements _SongLink { other.nativeAppUriDesktop == nativeAppUriDesktop)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, displayName, linkId, platform, show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop); - @JsonKey(ignore: true) + /// Create a copy of SongLink + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => @@ -313,8 +323,11 @@ abstract class _SongLink implements SongLink { String? get nativeAppUriMobile; @override String? get nativeAppUriDesktop; + + /// Create a copy of SongLink + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a6abdb20..a5b2ae93 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -170,7 +170,7 @@ abstract class SourcedTrack extends BasicSourcedTrack { /// /// If no sources match the codec, it will return the first or last source /// based on the user's audio quality preference. - String? getUrlOfCodec(SourceCodecs codec) { + TrackSource? getSourceOfCodec(SourceCodecs codec) { final preferences = ref.read(userPreferencesProvider); final exactMatch = sources.firstWhereOrNull( @@ -179,7 +179,7 @@ abstract class SourcedTrack extends BasicSourcedTrack { ); if (exactMatch != null) { - return exactMatch.url; + return exactMatch; } final sameCodecSources = sources @@ -193,8 +193,8 @@ abstract class SourcedTrack extends BasicSourcedTrack { if (sameCodecSources.isNotEmpty) { return preferences.audioQuality > SourceQualities.low - ? sameCodecSources.first.url - : sameCodecSources.last.url; + ? sameCodecSources.first + : sameCodecSources.last; } final fallbackSource = sources.sorted((a, b) { @@ -204,8 +204,12 @@ abstract class SourcedTrack extends BasicSourcedTrack { }); return preferences.audioQuality > SourceQualities.low - ? fallbackSource.firstOrNull?.url - : fallbackSource.lastOrNull?.url; + ? fallbackSource.firstOrNull + : fallbackSource.lastOrNull; + } + + String? getUrlOfCodec(SourceCodecs codec) { + return getSourceOfCodec(codec)?.url; } SourceCodecs get codec { @@ -220,12 +224,4 @@ abstract class SourcedTrack extends BasicSourcedTrack { _ => preferences.streamMusicCodec }; } - - TrackSource get activeTrackSource { - final audioQuality = ref.read(userPreferencesProvider).audioQuality; - return sources.firstWhereOrNull( - (source) => source.codec == codec && source.quality == audioQuality, - ) ?? - sources.first; - } } diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart index 93293bd3..6873d8f0 100644 --- a/lib/services/sourced_track/sources/dab_music.dart +++ b/lib/services/sourced_track/sources/dab_music.dart @@ -54,6 +54,7 @@ class DABMusicSourcedTrack extends SourcedTrack { static Future> fetchSources( String id, SourceQualities quality, + AudioQuality trackMaximumQuality, ) async { try { final isUncompressed = quality == SourceQualities.uncompressed; @@ -64,14 +65,26 @@ class DABMusicSourcedTrack extends SourcedTrack { if (streamResponse.url == null) { throw Exception("No stream URL found for track ID: $id"); } + + // kbps = (bitDepth * sampleRate * channels) / 1000 + final uncompressedBitrate = !isUncompressed + ? 0 + : ((trackMaximumQuality.maximumBitDepth ?? 0) * + ((trackMaximumQuality.maximumSamplingRate ?? 0) * 1000) * + 2) / + 1000; return [ TrackSource( url: streamResponse.url!, quality: isUncompressed ? SourceQualities.uncompressed : SourceQualities.high, - bitrate: isUncompressed ? "2998kbps" : "320kbps", + bitrate: + isUncompressed ? "${uncompressedBitrate.floor()}kbps" : "320kbps", codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3, + qualityLabel: isUncompressed + ? "${trackMaximumQuality.maximumBitDepth}bit • ${trackMaximumQuality.maximumSamplingRate}kHz • FLAC • Stereo" + : "MP3 • 320kbps • mp3 • Stereo", ), ]; } catch (e, stackTrace) { @@ -91,6 +104,7 @@ class DABMusicSourcedTrack extends SourcedTrack { source = await fetchSources( result.id.toString(), ref.read(userPreferencesProvider).audioQuality, + result.audioQuality!, ); } @@ -186,6 +200,11 @@ class DABMusicSourcedTrack extends SourcedTrack { final source = await fetchSources( sibling.id, ref.read(userPreferencesProvider).audioQuality, + const AudioQuality( + isHiRes: true, + maximumBitDepth: 16, + maximumSamplingRate: 44.1, + ), ); return DABMusicSourcedTrack( diff --git a/lib/services/sourced_track/sources/invidious.dart b/lib/services/sourced_track/sources/invidious.dart index 82e001f5..c5421355 100644 --- a/lib/services/sourced_track/sources/invidious.dart +++ b/lib/services/sourced_track/sources/invidious.dart @@ -94,6 +94,7 @@ class InvidiousSourcedTrack extends SourcedTrack { static List toSources(InvidiousVideoResponse manifest) { return manifest.adaptiveFormats.map((stream) { + var isWebm = stream.type.contains("audio/webm"); return TrackSource( url: stream.url.toString(), quality: switch (stream.qualityLabel) { @@ -101,10 +102,11 @@ class InvidiousSourcedTrack extends SourcedTrack { "medium" => SourceQualities.medium, _ => SourceQualities.low, }, - codec: stream.type.contains("audio/webm") - ? SourceCodecs.weba - : SourceCodecs.m4a, + codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, bitrate: stream.bitrate, + qualityLabel: + "${isWebm ? "Opus" : "AAC"} • ${stream.bitrate.replaceAll("kbps", "")}kbps " + "• ${isWebm ? "weba" : "m4a"} • Stereo", ); }).toList(); } diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index 02e97479..be78be25 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -104,6 +104,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { : SourceQualities.low, codec: SourceCodecs.m4a, bitrate: link.quality, + qualityLabel: "AAC • ${link.quality} • MP4 • Stereo", ); }).toList() ); diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 78beda10..fca6c623 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -98,6 +98,7 @@ class PipedSourcedTrack extends SourcedTrack { static List toSources(PipedStreamResponse manifest) { return manifest.audioStreams.map((audio) { + final isMp4 = audio.format == PipedAudioStreamFormat.m4a; return TrackSource( url: audio.url.toString(), quality: switch (audio.quality) { @@ -105,10 +106,11 @@ class PipedSourcedTrack extends SourcedTrack { "medium" => SourceQualities.medium, _ => SourceQualities.low, }, - codec: audio.format == PipedAudioStreamFormat.m4a - ? SourceCodecs.m4a - : SourceCodecs.weba, + codec: isMp4 ? SourceCodecs.m4a : SourceCodecs.weba, bitrate: audio.bitrate.toString(), + qualityLabel: + "${isMp4 ? "AAC" : "Opus"} • ${(audio.bitrate / 1000).floor()}kbps " + "• ${isMp4 ? "m4a" : "weba"} • Stereo", ); }).toList(); } diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 399d5e10..e3e9dd39 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -98,6 +98,7 @@ class YoutubeSourcedTrack extends SourcedTrack { static List toTrackSources(StreamManifest manifest) { return manifest.audioOnly.map((streamInfo) { + var isWebm = streamInfo.codec.mimeType == "audio/webm"; return TrackSource( url: streamInfo.url.toString(), quality: switch (streamInfo.qualityLabel) { @@ -106,10 +107,11 @@ class YoutubeSourcedTrack extends SourcedTrack { "low" => SourceQualities.low, _ => SourceQualities.high, }, - codec: streamInfo.codec.mimeType == "audio/webm" - ? SourceCodecs.weba - : SourceCodecs.m4a, + codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, bitrate: streamInfo.bitrate.bitsPerSecond.toString(), + qualityLabel: + "${isWebm ? "Opus" : "AAC"} • ${(streamInfo.bitrate.kiloBitsPerSecond).floor()}kbps " + "• ${isWebm ? "weba" : "m4a"} • Stereo", ); }).toList(); } diff --git a/untranslated_messages.json b/untranslated_messages.json index 2334c1b3..ba110540 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,147 +1,176 @@ { "ar": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "bn": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ca": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "cs": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "de": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "es": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "eu": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "fa": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "fi": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "fr": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "hi": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "id": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "it": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ja": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ka": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ko": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ne": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "nl": [ "audio_source", "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "pl": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "pt": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ru": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "ta": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "th": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "tl": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "tr": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "uk": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "vi": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "zh": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ], "zh_TW": [ "source", - "uncompressed" + "uncompressed", + "dab_music_source_description" ] } From e5150515f3649bd2d0626696b61db4a7058bc351 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 20 Sep 2025 17:54:08 +0600 Subject: [PATCH 10/47] chore: cache dab music match source --- .../sourced_track/sources/dab_music.dart | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart index 6873d8f0..83cc55b4 100644 --- a/lib/services/sourced_track/sources/dab_music.dart +++ b/lib/services/sourced_track/sources/dab_music.dart @@ -1,8 +1,12 @@ +import 'dart:convert'; + import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -32,11 +36,68 @@ class DABMusicSourcedTrack extends SourcedTrack { required Ref ref, }) async { try { + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(query.id)) + ..limit(1) + ..orderBy([ + (s) => OrderingTerm( + expression: s.createdAt, + mode: OrderingMode.desc, + ), + ])) + .get() + .then((s) => s.firstOrNull); + + if (cachedSource != null && + cachedSource.sourceType == SourceType.dabMusic) { + final json = jsonDecode(cachedSource.sourceId); + final info = TrackSourceInfo.fromJson(json["info"]); + final source = (json["sources"] as List?) + ?.map((s) => TrackSource.fromJson(s)) + .toList(); + + final [updatedSource] = await fetchSources( + info.id, + ref.read(userPreferencesProvider).audioQuality, + const AudioQuality( + isHiRes: true, + maximumBitDepth: 16, + maximumSamplingRate: 44.1, + ), + ); + + return DABMusicSourcedTrack( + ref: ref, + source: AudioSource.dabMusic, + siblings: [], + info: info, + query: query, + sources: [ + source!.first.copyWith(url: updatedSource.url), + ], + ); + } + final siblings = await fetchSiblings(ref: ref, query: query); if (siblings.isEmpty) { throw TrackNotFoundError(query); } + + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + sourceId: jsonEncode({ + "info": siblings.first.info.toJson(), + "sources": (siblings.first.source ?? []) + .map((s) => s.toJson()) + .toList(), + }), + sourceType: const Value(SourceType.dabMusic), + ), + ); + return DABMusicSourcedTrack( ref: ref, siblings: siblings.map((s) => s.info).skip(1).toList(), @@ -207,6 +268,23 @@ class DABMusicSourcedTrack extends SourcedTrack { ), ); + final database = ref.read(databaseProvider); + + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + sourceId: jsonEncode({ + "info": newSourceInfo.toJson(), + "sources": source.map((s) => s.toJson()).toList(), + }), + sourceType: const Value(SourceType.dabMusic), + // Because we're sorting by createdAt in the query + // we have to update it to indicate priority + createdAt: Value(DateTime.now()), + ), + mode: InsertMode.replace, + ); + return DABMusicSourcedTrack( ref: ref, siblings: newSiblings, From 7d849b14301a1b646f0d4a9f86d6db4c85c4172a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 20 Sep 2025 18:01:42 +0600 Subject: [PATCH 11/47] fix: change plugin download directory to application support --- .../getting_started/sections/playback.dart | 23 ++++++++++++------- .../metadata_plugin_provider.dart | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index 6e1a29f4..a6f887cb 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' show Badge; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -83,14 +84,20 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { runSpacing: 6, children: [ for (final source in AudioSource.values) - RadioCard( - value: source, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - audioSourceToIconMap[source]!, - Text(source.label), - ], + Badge( + isLabelVisible: source == AudioSource.dabMusic, + label: const Text("NEW"), + backgroundColor: Colors.lime[300], + textColor: Colors.black, + child: RadioCard( + value: source, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + audioSourceToIconMap[source]!, + Text(source.label), + ], + ), ), ), ], diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index 881c0113..cf19c1f5 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -214,7 +214,7 @@ class MetadataPluginNotifier extends AsyncNotifier { /// Root directory where all metadata plugins are stored. Future _getPluginRootDir() async => Directory( join( - (await getApplicationCacheDirectory()).path, + (await getApplicationSupportDirectory()).path, "metadata-plugins", ), ); From 973ca20c8e9f86891ac6c2e17a57a12a11e454ac Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 20 Sep 2025 20:25:51 +0600 Subject: [PATCH 12/47] fix(playback): play next not working --- lib/components/track_tile/track_options.dart | 29 ++++++++++--------- lib/provider/audio_player/audio_player.dart | 12 ++++---- .../track_options/track_options_provider.dart | 2 +- lib/services/audio_player/audio_player.dart | 1 + lib/services/audio_player/custom_player.dart | 20 +++++++++++-- pubspec.lock | 4 +-- pubspec.yaml | 2 +- 7 files changed, 44 insertions(+), 26 deletions(-) diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 7943fe3d..6124abf0 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -59,7 +60,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.delete, playlistId, ); @@ -73,7 +74,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.album, playlistId, ); @@ -97,7 +98,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.addToQueue, playlistId, ); @@ -110,7 +111,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.playNext, playlistId, ); @@ -124,7 +125,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.removeFromQueue, playlistId, ); @@ -139,7 +140,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.favorite, playlistId, ); @@ -162,7 +163,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.startRadio, playlistId, ); @@ -175,7 +176,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.addToPlaylist, playlistId, ); @@ -190,7 +191,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.removeFromPlaylist, playlistId, ); @@ -204,7 +205,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.download, playlistId, ); @@ -226,7 +227,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.blacklist, playlistId, ); @@ -250,7 +251,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.share, playlistId, ); @@ -264,7 +265,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.songlink, playlistId, ); @@ -282,7 +283,7 @@ class TrackOptions extends HookConsumerWidget { style: ButtonVariance.menu, onPressed: () async { await trackOptionActions.action( - context, + rootNavigatorKey.currentContext!, TrackOptionValue.details, playlistId, ); diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index cb53ca4f..5db28125 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -259,11 +259,14 @@ class AudioPlayerNotifier extends Notifier { return addTracks(tracks); } - final addableTracks = _blacklist.filter(tracks).where( + final addableTracks = _blacklist + .filter(tracks) + .where( (track) => allowDuplicates || !state.tracks.any((element) => _compareTracks(element, track)), - ); + ) + .toList(); state = state.copyWith( tracks: [...addableTracks, ...state.tracks], @@ -371,13 +374,12 @@ class AudioPlayerNotifier extends Notifier { } bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) { - if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) || - (a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) { + if (a.runtimeType != b.runtimeType) { return false; } return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject - ? (a).path == (b).path + ? a.path == b.path : a.id == b.id; } diff --git a/lib/provider/track_options/track_options_provider.dart b/lib/provider/track_options/track_options_provider.dart index 7e6bc16e..42f363d9 100644 --- a/lib/provider/track_options/track_options_provider.dart +++ b/lib/provider/track_options/track_options_provider.dart @@ -166,7 +166,7 @@ class TrackOptionsActions { } break; case TrackOptionValue.playNext: - playback.addTracksAtFirst([track]); + await playback.addTracksAtFirst([track]); if (context.mounted) { showToast( diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 925d0761..262b9d10 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -57,6 +57,7 @@ abstract class AudioPlayerInterface { title: "Spotube", logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, bufferSize: 4 * 1024 * 1024, // 4MB buffer + async: true, ), ) { _mkPlayer.stream.error.listen((event) { diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index 39866dcc..7cbd51a5 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -121,9 +121,23 @@ class CustomPlayer extends Player { NativePlayer get nativePlayer => platform as NativePlayer; Future insert(int index, Media media) async { - await add(media); - await Future.delayed(const Duration(milliseconds: 100)); - await move(state.playlist.medias.length - 1, index); + final addedMediaCompleter = Completer(); + final playlistStream = stream.playlist.listen( + (event) { + final mediaAddedIndex = + event.medias.indexWhere((m) => m.uri == media.uri); + if (mediaAddedIndex != -1 && !addedMediaCompleter.isCompleted) { + addedMediaCompleter.complete(mediaAddedIndex); + } + }, + ); + try { + await add(media); + final mediaAddedIndex = await addedMediaCompleter.future; + await move(mediaAddedIndex, index); + } finally { + playlistStream.cancel(); + } } Future setAudioNormalization(bool normalize) async { diff --git a/pubspec.lock b/pubspec.lock index 0ccd5c44..bb8bb234 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1412,10 +1412,10 @@ packages: dependency: "direct main" description: name: invidious - sha256: "27ef3a001df875665de15535dbc9099f44d12a59480018fb1e17377d4af0308d" + sha256: "0da8ebc4c4110057f03302bbd54514b10642154d7be569e7994172f2202dcfe8" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" io: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0877d736..7ea73e04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,7 +81,7 @@ dependencies: http: ^1.2.1 image_picker: ^1.1.0 intl: any - invidious: ^0.1.1 + invidious: ^0.1.2 jiosaavn: ^0.1.0 json_annotation: ^4.8.1 local_notifier: ^0.1.6 From 348c2e931baac22310fd3160219c4b387d6d5244 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Oct 2025 10:27:00 +0600 Subject: [PATCH 13/47] fix: upgrade NewPipeExtractor to avoid unplayable streams --- android/app/build.gradle | 1 + android/settings.gradle | 3 ++- pubspec.lock | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ee481eca..d8e35b29 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,6 +2,7 @@ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" + id "org.jetbrains.kotlin.plugin.compose" } def localProperties = new Properties() diff --git a/android/settings.gradle b/android/settings.gradle index 1e8ffbe3..53d34a77 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.7.0' apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "org.jetbrains.kotlin.plugin.compose" version "2.1.0" apply false } include ':app' \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index bb8bb234..57f17a15 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -972,7 +972,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "916bde44cbead75125e8db842eb46bdcf211a79a" + resolved-ref: d4d71545111c8ca6c91f0040091c42d74cce1762 url: "https://github.com/KRTirtho/flutter_new_pipe_extractor.git" source: git version: "0.1.0" From 88699e9a3b692ccc0d54a276cfc14aa5ab5364f6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Oct 2025 11:26:58 +0600 Subject: [PATCH 14/47] fix: jiosaavn not working due to json signature change --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 57f17a15..ff0c689c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1444,10 +1444,10 @@ packages: dependency: "direct main" description: name: jiosaavn - sha256: d32b4f43f26488f942f5d7d19d748a1f2664ae3d41ff9c7d50eeb81705174bd2 + sha256: b6bde15c56398ebfd439825a64fb540a265773d1a518ba103e79988e13d16e1d url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" jovial_misc: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ea73e04..3cc1eb05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,7 +82,7 @@ dependencies: image_picker: ^1.1.0 intl: any invidious: ^0.1.2 - jiosaavn: ^0.1.0 + jiosaavn: ^0.1.1 json_annotation: ^4.8.1 local_notifier: ^0.1.6 logger: ^2.0.2 From 439de5d7f7b61492e5cb6dfa4e8f3439993f4a85 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 19 Oct 2025 13:48:53 +0600 Subject: [PATCH 15/47] feat: add plugin audio source models and api service --- lib/models/metadata/audio_source.dart | 84 + lib/models/metadata/metadata.dart | 1 + lib/models/metadata/metadata.freezed.dart | 1527 ++++++++++++++++- lib/models/metadata/metadata.g.dart | 125 +- lib/models/metadata/plugin.dart | 11 +- .../metadata_plugin_provider.dart | 9 +- .../metadata/endpoints/audio_source.dart | 38 + lib/services/metadata/metadata.dart | 63 +- pubspec.lock | 13 +- pubspec.yaml | 3 +- 10 files changed, 1827 insertions(+), 47 deletions(-) create mode 100644 lib/models/metadata/audio_source.dart create mode 100644 lib/services/metadata/endpoints/audio_source.dart diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart new file mode 100644 index 00000000..c429ec74 --- /dev/null +++ b/lib/models/metadata/audio_source.dart @@ -0,0 +1,84 @@ +part of 'metadata.dart'; + +enum SpotubeMediaCompressionType { + lossy, + lossless, +} + +@Freezed(unionKey: 'type') +class SpotubeAudioSourceContainerPreset + with _$SpotubeAudioSourceContainerPreset { + @FreezedUnionValue("lossy") + factory SpotubeAudioSourceContainerPreset.lossy({ + required SpotubeMediaCompressionType type, + required String name, + required List qualities, + }) = SpotubeAudioSourceContainerPresetLossy; + + @FreezedUnionValue("lossless") + factory SpotubeAudioSourceContainerPreset.lossless({ + required SpotubeMediaCompressionType type, + required String name, + required List qualities, + }) = SpotubeAudioSourceContainerPresetLossless; + + factory SpotubeAudioSourceContainerPreset.fromJson( + Map json) => + _$SpotubeAudioSourceContainerPresetFromJson(json); +} + +@freezed +class SpotubeAudioLossyContainerQuality + with _$SpotubeAudioLossyContainerQuality { + factory SpotubeAudioLossyContainerQuality({ + required double bitrate, + }) = _SpotubeAudioLossyContainerQuality; + + factory SpotubeAudioLossyContainerQuality.fromJson( + Map json) => + _$SpotubeAudioLossyContainerQualityFromJson(json); +} + +@freezed +class SpotubeAudioLosslessContainerQuality + with _$SpotubeAudioLosslessContainerQuality { + factory SpotubeAudioLosslessContainerQuality({ + required int bitDepth, + required double sampleRate, + }) = _SpotubeAudioLosslessContainerQuality; + + factory SpotubeAudioLosslessContainerQuality.fromJson( + Map json) => + _$SpotubeAudioLosslessContainerQualityFromJson(json); +} + +@freezed +class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject { + factory SpotubeAudioSourceMatchObject({ + required String id, + required String title, + required List artists, + required Duration duration, + String? thumbnail, + required String externalUri, + }) = _SpotubeAudioSourceMatchObject; + + factory SpotubeAudioSourceMatchObject.fromJson(Map json) => + _$SpotubeAudioSourceMatchObjectFromJson(json); +} + +@freezed +class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject { + factory SpotubeAudioSourceStreamObject({ + required String url, + required String container, + required SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate, + }) = _SpotubeAudioSourceStreamObject; + + factory SpotubeAudioSourceStreamObject.fromJson(Map json) => + _$SpotubeAudioSourceStreamObjectFromJson(json); +} diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index 97da704c..4c6eb2ac 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -15,6 +15,7 @@ import 'package:spotube/utils/primitive_utils.dart'; part 'metadata.g.dart'; part 'metadata.freezed.dart'; +part 'audio_source.dart'; part 'album.dart'; part 'artist.dart'; part 'browse.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index bb4cf3f8..5d4bc695 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -14,6 +14,1502 @@ T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); +SpotubeAudioSourceContainerPreset _$SpotubeAudioSourceContainerPresetFromJson( + Map json) { + switch (json['type']) { + case 'lossy': + return SpotubeAudioSourceContainerPresetLossy.fromJson(json); + case 'lossless': + return SpotubeAudioSourceContainerPresetLossless.fromJson(json); + + default: + throw CheckedFromJsonException( + json, + 'type', + 'SpotubeAudioSourceContainerPreset', + 'Invalid union type "${json['type']}"!'); + } +} + +/// @nodoc +mixin _$SpotubeAudioSourceContainerPreset { + SpotubeMediaCompressionType get type => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + List get qualities => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceContainerPreset to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceContainerPresetCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory $SpotubeAudioSourceContainerPresetCopyWith( + SpotubeAudioSourceContainerPreset value, + $Res Function(SpotubeAudioSourceContainerPreset) then) = + _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + SpotubeAudioSourceContainerPreset>; + @useResult + $Res call({SpotubeMediaCompressionType type, String name}); +} + +/// @nodoc +class _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceContainerPreset> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + _$SpotubeAudioSourceContainerPresetCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith<$Res> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith( + _$SpotubeAudioSourceContainerPresetLossyImpl value, + $Res Function(_$SpotubeAudioSourceContainerPresetLossyImpl) then) = + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SpotubeMediaCompressionType type, + String name, + List qualities}); +} + +/// @nodoc +class __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + _$SpotubeAudioSourceContainerPresetLossyImpl> + implements _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith<$Res> { + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl( + _$SpotubeAudioSourceContainerPresetLossyImpl _value, + $Res Function(_$SpotubeAudioSourceContainerPresetLossyImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? qualities = null, + }) { + return _then(_$SpotubeAudioSourceContainerPresetLossyImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qualities: null == qualities + ? _value._qualities + : qualities // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceContainerPresetLossyImpl + implements SpotubeAudioSourceContainerPresetLossy { + _$SpotubeAudioSourceContainerPresetLossyImpl( + {required this.type, + required this.name, + required final List qualities}) + : _qualities = qualities; + + factory _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceContainerPresetLossyImplFromJson(json); + + @override + final SpotubeMediaCompressionType type; + @override + final String name; + final List _qualities; + @override + List get qualities { + if (_qualities is EqualUnmodifiableListView) return _qualities; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_qualities); + } + + @override + String toString() { + return 'SpotubeAudioSourceContainerPreset.lossy(type: $type, name: $name, qualities: $qualities)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceContainerPresetLossyImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.name, name) || other.name == name) && + const DeepCollectionEquality() + .equals(other._qualities, _qualities)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, type, name, const DeepCollectionEquality().hash(_qualities)); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith< + _$SpotubeAudioSourceContainerPresetLossyImpl> + get copyWith => + __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl< + _$SpotubeAudioSourceContainerPresetLossyImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) { + return lossy(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) { + return lossy?.call(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) { + if (lossy != null) { + return lossy(type, name, qualities); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) { + return lossy(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) { + return lossy?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) { + if (lossy != null) { + return lossy(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$SpotubeAudioSourceContainerPresetLossyImplToJson( + this, + ); + } +} + +abstract class SpotubeAudioSourceContainerPresetLossy + implements SpotubeAudioSourceContainerPreset { + factory SpotubeAudioSourceContainerPresetLossy( + {required final SpotubeMediaCompressionType type, + required final String name, + required final List qualities}) = + _$SpotubeAudioSourceContainerPresetLossyImpl; + + factory SpotubeAudioSourceContainerPresetLossy.fromJson( + Map json) = + _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson; + + @override + SpotubeMediaCompressionType get type; + @override + String get name; + @override + List get qualities; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceContainerPresetLossyImplCopyWith< + _$SpotubeAudioSourceContainerPresetLossyImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith<$Res> + implements $SpotubeAudioSourceContainerPresetCopyWith<$Res> { + factory _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith( + _$SpotubeAudioSourceContainerPresetLosslessImpl value, + $Res Function(_$SpotubeAudioSourceContainerPresetLosslessImpl) then) = + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SpotubeMediaCompressionType type, + String name, + List qualities}); +} + +/// @nodoc +class __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceContainerPresetCopyWithImpl<$Res, + _$SpotubeAudioSourceContainerPresetLosslessImpl> + implements _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith<$Res> { + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl( + _$SpotubeAudioSourceContainerPresetLosslessImpl _value, + $Res Function(_$SpotubeAudioSourceContainerPresetLosslessImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? name = null, + Object? qualities = null, + }) { + return _then(_$SpotubeAudioSourceContainerPresetLosslessImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qualities: null == qualities + ? _value._qualities + : qualities // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceContainerPresetLosslessImpl + implements SpotubeAudioSourceContainerPresetLossless { + _$SpotubeAudioSourceContainerPresetLosslessImpl( + {required this.type, + required this.name, + required final List qualities}) + : _qualities = qualities; + + factory _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(json); + + @override + final SpotubeMediaCompressionType type; + @override + final String name; + final List _qualities; + @override + List get qualities { + if (_qualities is EqualUnmodifiableListView) return _qualities; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_qualities); + } + + @override + String toString() { + return 'SpotubeAudioSourceContainerPreset.lossless(type: $type, name: $name, qualities: $qualities)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceContainerPresetLosslessImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.name, name) || other.name == name) && + const DeepCollectionEquality() + .equals(other._qualities, _qualities)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, type, name, const DeepCollectionEquality().hash(_qualities)); + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith< + _$SpotubeAudioSourceContainerPresetLosslessImpl> + get copyWith => + __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl< + _$SpotubeAudioSourceContainerPresetLosslessImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossy, + required TResult Function(SpotubeMediaCompressionType type, String name, + List qualities) + lossless, + }) { + return lossless(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult? Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + }) { + return lossless?.call(type, name, qualities); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossy, + TResult Function(SpotubeMediaCompressionType type, String name, + List qualities)? + lossless, + required TResult orElse(), + }) { + if (lossless != null) { + return lossless(type, name, qualities); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(SpotubeAudioSourceContainerPresetLossy value) + lossy, + required TResult Function(SpotubeAudioSourceContainerPresetLossless value) + lossless, + }) { + return lossless(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult? Function(SpotubeAudioSourceContainerPresetLossless value)? + lossless, + }) { + return lossless?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(SpotubeAudioSourceContainerPresetLossy value)? lossy, + TResult Function(SpotubeAudioSourceContainerPresetLossless value)? lossless, + required TResult orElse(), + }) { + if (lossless != null) { + return lossless(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( + this, + ); + } +} + +abstract class SpotubeAudioSourceContainerPresetLossless + implements SpotubeAudioSourceContainerPreset { + factory SpotubeAudioSourceContainerPresetLossless( + {required final SpotubeMediaCompressionType type, + required final String name, + required final List + qualities}) = _$SpotubeAudioSourceContainerPresetLosslessImpl; + + factory SpotubeAudioSourceContainerPresetLossless.fromJson( + Map json) = + _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson; + + @override + SpotubeMediaCompressionType get type; + @override + String get name; + @override + List get qualities; + + /// Create a copy of SpotubeAudioSourceContainerPreset + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceContainerPresetLosslessImplCopyWith< + _$SpotubeAudioSourceContainerPresetLosslessImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioLossyContainerQuality _$SpotubeAudioLossyContainerQualityFromJson( + Map json) { + return _SpotubeAudioLossyContainerQuality.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioLossyContainerQuality { + double get bitrate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioLossyContainerQuality to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioLossyContainerQualityCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + factory $SpotubeAudioLossyContainerQualityCopyWith( + SpotubeAudioLossyContainerQuality value, + $Res Function(SpotubeAudioLossyContainerQuality) then) = + _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + SpotubeAudioLossyContainerQuality>; + @useResult + $Res call({double bitrate}); +} + +/// @nodoc +class _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + $Val extends SpotubeAudioLossyContainerQuality> + implements $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + _$SpotubeAudioLossyContainerQualityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitrate = null, + }) { + return _then(_value.copyWith( + bitrate: null == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> + implements $SpotubeAudioLossyContainerQualityCopyWith<$Res> { + factory _$$SpotubeAudioLossyContainerQualityImplCopyWith( + _$SpotubeAudioLossyContainerQualityImpl value, + $Res Function(_$SpotubeAudioLossyContainerQualityImpl) then) = + __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({double bitrate}); +} + +/// @nodoc +class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> + extends _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, + _$SpotubeAudioLossyContainerQualityImpl> + implements _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> { + __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl( + _$SpotubeAudioLossyContainerQualityImpl _value, + $Res Function(_$SpotubeAudioLossyContainerQualityImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitrate = null, + }) { + return _then(_$SpotubeAudioLossyContainerQualityImpl( + bitrate: null == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioLossyContainerQualityImpl + implements _SpotubeAudioLossyContainerQuality { + _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}); + + factory _$SpotubeAudioLossyContainerQualityImpl.fromJson( + Map json) => + _$$SpotubeAudioLossyContainerQualityImplFromJson(json); + + @override + final double bitrate; + + @override + String toString() { + return 'SpotubeAudioLossyContainerQuality(bitrate: $bitrate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioLossyContainerQualityImpl && + (identical(other.bitrate, bitrate) || other.bitrate == bitrate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, bitrate); + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioLossyContainerQualityImplCopyWith< + _$SpotubeAudioLossyContainerQualityImpl> + get copyWith => __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl< + _$SpotubeAudioLossyContainerQualityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioLossyContainerQualityImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioLossyContainerQuality + implements SpotubeAudioLossyContainerQuality { + factory _SpotubeAudioLossyContainerQuality({required final double bitrate}) = + _$SpotubeAudioLossyContainerQualityImpl; + + factory _SpotubeAudioLossyContainerQuality.fromJson( + Map json) = + _$SpotubeAudioLossyContainerQualityImpl.fromJson; + + @override + double get bitrate; + + /// Create a copy of SpotubeAudioLossyContainerQuality + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioLossyContainerQualityImplCopyWith< + _$SpotubeAudioLossyContainerQualityImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioLosslessContainerQuality + _$SpotubeAudioLosslessContainerQualityFromJson(Map json) { + return _SpotubeAudioLosslessContainerQuality.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioLosslessContainerQuality { + int get bitDepth => throw _privateConstructorUsedError; + double get sampleRate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioLosslessContainerQualityCopyWith< + SpotubeAudioLosslessContainerQuality> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + factory $SpotubeAudioLosslessContainerQualityCopyWith( + SpotubeAudioLosslessContainerQuality value, + $Res Function(SpotubeAudioLosslessContainerQuality) then) = + _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + SpotubeAudioLosslessContainerQuality>; + @useResult + $Res call({int bitDepth, double sampleRate}); +} + +/// @nodoc +class _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + $Val extends SpotubeAudioLosslessContainerQuality> + implements $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + _$SpotubeAudioLosslessContainerQualityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitDepth = null, + Object? sampleRate = null, + }) { + return _then(_value.copyWith( + bitDepth: null == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int, + sampleRate: null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> + implements $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { + factory _$$SpotubeAudioLosslessContainerQualityImplCopyWith( + _$SpotubeAudioLosslessContainerQualityImpl value, + $Res Function(_$SpotubeAudioLosslessContainerQualityImpl) then) = + __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int bitDepth, double sampleRate}); +} + +/// @nodoc +class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> + extends _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, + _$SpotubeAudioLosslessContainerQualityImpl> + implements _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> { + __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl( + _$SpotubeAudioLosslessContainerQualityImpl _value, + $Res Function(_$SpotubeAudioLosslessContainerQualityImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bitDepth = null, + Object? sampleRate = null, + }) { + return _then(_$SpotubeAudioLosslessContainerQualityImpl( + bitDepth: null == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int, + sampleRate: null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioLosslessContainerQualityImpl + implements _SpotubeAudioLosslessContainerQuality { + _$SpotubeAudioLosslessContainerQualityImpl( + {required this.bitDepth, required this.sampleRate}); + + factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson( + Map json) => + _$$SpotubeAudioLosslessContainerQualityImplFromJson(json); + + @override + final int bitDepth; + @override + final double sampleRate; + + @override + String toString() { + return 'SpotubeAudioLosslessContainerQuality(bitDepth: $bitDepth, sampleRate: $sampleRate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioLosslessContainerQualityImpl && + (identical(other.bitDepth, bitDepth) || + other.bitDepth == bitDepth) && + (identical(other.sampleRate, sampleRate) || + other.sampleRate == sampleRate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, bitDepth, sampleRate); + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioLosslessContainerQualityImplCopyWith< + _$SpotubeAudioLosslessContainerQualityImpl> + get copyWith => __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl< + _$SpotubeAudioLosslessContainerQualityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioLosslessContainerQualityImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioLosslessContainerQuality + implements SpotubeAudioLosslessContainerQuality { + factory _SpotubeAudioLosslessContainerQuality( + {required final int bitDepth, required final double sampleRate}) = + _$SpotubeAudioLosslessContainerQualityImpl; + + factory _SpotubeAudioLosslessContainerQuality.fromJson( + Map json) = + _$SpotubeAudioLosslessContainerQualityImpl.fromJson; + + @override + int get bitDepth; + @override + double get sampleRate; + + /// Create a copy of SpotubeAudioLosslessContainerQuality + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioLosslessContainerQualityImplCopyWith< + _$SpotubeAudioLosslessContainerQualityImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioSourceMatchObject _$SpotubeAudioSourceMatchObjectFromJson( + Map json) { + return _SpotubeAudioSourceMatchObject.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioSourceMatchObject { + String get id => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + List get artists => throw _privateConstructorUsedError; + Duration get duration => throw _privateConstructorUsedError; + String? get thumbnail => throw _privateConstructorUsedError; + String get externalUri => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceMatchObject to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceMatchObjectCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + factory $SpotubeAudioSourceMatchObjectCopyWith( + SpotubeAudioSourceMatchObject value, + $Res Function(SpotubeAudioSourceMatchObject) then) = + _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + SpotubeAudioSourceMatchObject>; + @useResult + $Res call( + {String id, + String title, + List artists, + Duration duration, + String? thumbnail, + String externalUri}); +} + +/// @nodoc +class _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceMatchObject> + implements $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + _$SpotubeAudioSourceMatchObjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? artists = null, + Object? duration = null, + Object? thumbnail = freezed, + Object? externalUri = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + artists: null == artists + ? _value.artists + : artists // ignore: cast_nullable_to_non_nullable + as List, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + thumbnail: freezed == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String?, + externalUri: null == externalUri + ? _value.externalUri + : externalUri // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceMatchObjectImplCopyWith<$Res> + implements $SpotubeAudioSourceMatchObjectCopyWith<$Res> { + factory _$$SpotubeAudioSourceMatchObjectImplCopyWith( + _$SpotubeAudioSourceMatchObjectImpl value, + $Res Function(_$SpotubeAudioSourceMatchObjectImpl) then) = + __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String title, + List artists, + Duration duration, + String? thumbnail, + String externalUri}); +} + +/// @nodoc +class __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceMatchObjectCopyWithImpl<$Res, + _$SpotubeAudioSourceMatchObjectImpl> + implements _$$SpotubeAudioSourceMatchObjectImplCopyWith<$Res> { + __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl( + _$SpotubeAudioSourceMatchObjectImpl _value, + $Res Function(_$SpotubeAudioSourceMatchObjectImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? artists = null, + Object? duration = null, + Object? thumbnail = freezed, + Object? externalUri = null, + }) { + return _then(_$SpotubeAudioSourceMatchObjectImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + artists: null == artists + ? _value._artists + : artists // ignore: cast_nullable_to_non_nullable + as List, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + thumbnail: freezed == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String?, + externalUri: null == externalUri + ? _value.externalUri + : externalUri // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceMatchObjectImpl + implements _SpotubeAudioSourceMatchObject { + _$SpotubeAudioSourceMatchObjectImpl( + {required this.id, + required this.title, + required final List artists, + required this.duration, + this.thumbnail, + required this.externalUri}) + : _artists = artists; + + factory _$SpotubeAudioSourceMatchObjectImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceMatchObjectImplFromJson(json); + + @override + final String id; + @override + final String title; + final List _artists; + @override + List get artists { + if (_artists is EqualUnmodifiableListView) return _artists; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_artists); + } + + @override + final Duration duration; + @override + final String? thumbnail; + @override + final String externalUri; + + @override + String toString() { + return 'SpotubeAudioSourceMatchObject(id: $id, title: $title, artists: $artists, duration: $duration, thumbnail: $thumbnail, externalUri: $externalUri)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceMatchObjectImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + const DeepCollectionEquality().equals(other._artists, _artists) && + (identical(other.duration, duration) || + other.duration == duration) && + (identical(other.thumbnail, thumbnail) || + other.thumbnail == thumbnail) && + (identical(other.externalUri, externalUri) || + other.externalUri == externalUri)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + title, + const DeepCollectionEquality().hash(_artists), + duration, + thumbnail, + externalUri); + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceMatchObjectImplCopyWith< + _$SpotubeAudioSourceMatchObjectImpl> + get copyWith => __$$SpotubeAudioSourceMatchObjectImplCopyWithImpl< + _$SpotubeAudioSourceMatchObjectImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioSourceMatchObjectImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioSourceMatchObject + implements SpotubeAudioSourceMatchObject { + factory _SpotubeAudioSourceMatchObject( + {required final String id, + required final String title, + required final List artists, + required final Duration duration, + final String? thumbnail, + required final String externalUri}) = _$SpotubeAudioSourceMatchObjectImpl; + + factory _SpotubeAudioSourceMatchObject.fromJson(Map json) = + _$SpotubeAudioSourceMatchObjectImpl.fromJson; + + @override + String get id; + @override + String get title; + @override + List get artists; + @override + Duration get duration; + @override + String? get thumbnail; + @override + String get externalUri; + + /// Create a copy of SpotubeAudioSourceMatchObject + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceMatchObjectImplCopyWith< + _$SpotubeAudioSourceMatchObjectImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SpotubeAudioSourceStreamObject _$SpotubeAudioSourceStreamObjectFromJson( + Map json) { + return _SpotubeAudioSourceStreamObject.fromJson(json); +} + +/// @nodoc +mixin _$SpotubeAudioSourceStreamObject { + String get url => throw _privateConstructorUsedError; + String get container => throw _privateConstructorUsedError; + SpotubeMediaCompressionType get type => throw _privateConstructorUsedError; + String? get codec => throw _privateConstructorUsedError; + double? get bitrate => throw _privateConstructorUsedError; + int? get bitDepth => throw _privateConstructorUsedError; + double? get sampleRate => throw _privateConstructorUsedError; + + /// Serializes this SpotubeAudioSourceStreamObject to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SpotubeAudioSourceStreamObjectCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + factory $SpotubeAudioSourceStreamObjectCopyWith( + SpotubeAudioSourceStreamObject value, + $Res Function(SpotubeAudioSourceStreamObject) then) = + _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + SpotubeAudioSourceStreamObject>; + @useResult + $Res call( + {String url, + String container, + SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate}); +} + +/// @nodoc +class _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + $Val extends SpotubeAudioSourceStreamObject> + implements $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + _$SpotubeAudioSourceStreamObjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? container = null, + Object? type = null, + Object? codec = freezed, + Object? bitrate = freezed, + Object? bitDepth = freezed, + Object? sampleRate = freezed, + }) { + return _then(_value.copyWith( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + container: null == container + ? _value.container + : container // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + codec: freezed == codec + ? _value.codec + : codec // ignore: cast_nullable_to_non_nullable + as String?, + bitrate: freezed == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double?, + bitDepth: freezed == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int?, + sampleRate: freezed == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SpotubeAudioSourceStreamObjectImplCopyWith<$Res> + implements $SpotubeAudioSourceStreamObjectCopyWith<$Res> { + factory _$$SpotubeAudioSourceStreamObjectImplCopyWith( + _$SpotubeAudioSourceStreamObjectImpl value, + $Res Function(_$SpotubeAudioSourceStreamObjectImpl) then) = + __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String url, + String container, + SpotubeMediaCompressionType type, + String? codec, + double? bitrate, + int? bitDepth, + double? sampleRate}); +} + +/// @nodoc +class __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl<$Res> + extends _$SpotubeAudioSourceStreamObjectCopyWithImpl<$Res, + _$SpotubeAudioSourceStreamObjectImpl> + implements _$$SpotubeAudioSourceStreamObjectImplCopyWith<$Res> { + __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl( + _$SpotubeAudioSourceStreamObjectImpl _value, + $Res Function(_$SpotubeAudioSourceStreamObjectImpl) _then) + : super(_value, _then); + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? container = null, + Object? type = null, + Object? codec = freezed, + Object? bitrate = freezed, + Object? bitDepth = freezed, + Object? sampleRate = freezed, + }) { + return _then(_$SpotubeAudioSourceStreamObjectImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + container: null == container + ? _value.container + : container // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as SpotubeMediaCompressionType, + codec: freezed == codec + ? _value.codec + : codec // ignore: cast_nullable_to_non_nullable + as String?, + bitrate: freezed == bitrate + ? _value.bitrate + : bitrate // ignore: cast_nullable_to_non_nullable + as double?, + bitDepth: freezed == bitDepth + ? _value.bitDepth + : bitDepth // ignore: cast_nullable_to_non_nullable + as int?, + sampleRate: freezed == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as double?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SpotubeAudioSourceStreamObjectImpl + implements _SpotubeAudioSourceStreamObject { + _$SpotubeAudioSourceStreamObjectImpl( + {required this.url, + required this.container, + required this.type, + this.codec, + this.bitrate, + this.bitDepth, + this.sampleRate}); + + factory _$SpotubeAudioSourceStreamObjectImpl.fromJson( + Map json) => + _$$SpotubeAudioSourceStreamObjectImplFromJson(json); + + @override + final String url; + @override + final String container; + @override + final SpotubeMediaCompressionType type; + @override + final String? codec; + @override + final double? bitrate; + @override + final int? bitDepth; + @override + final double? sampleRate; + + @override + String toString() { + return 'SpotubeAudioSourceStreamObject(url: $url, container: $container, type: $type, codec: $codec, bitrate: $bitrate, bitDepth: $bitDepth, sampleRate: $sampleRate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpotubeAudioSourceStreamObjectImpl && + (identical(other.url, url) || other.url == url) && + (identical(other.container, container) || + other.container == container) && + (identical(other.type, type) || other.type == type) && + (identical(other.codec, codec) || other.codec == codec) && + (identical(other.bitrate, bitrate) || other.bitrate == bitrate) && + (identical(other.bitDepth, bitDepth) || + other.bitDepth == bitDepth) && + (identical(other.sampleRate, sampleRate) || + other.sampleRate == sampleRate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, url, container, type, codec, bitrate, bitDepth, sampleRate); + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SpotubeAudioSourceStreamObjectImplCopyWith< + _$SpotubeAudioSourceStreamObjectImpl> + get copyWith => __$$SpotubeAudioSourceStreamObjectImplCopyWithImpl< + _$SpotubeAudioSourceStreamObjectImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SpotubeAudioSourceStreamObjectImplToJson( + this, + ); + } +} + +abstract class _SpotubeAudioSourceStreamObject + implements SpotubeAudioSourceStreamObject { + factory _SpotubeAudioSourceStreamObject( + {required final String url, + required final String container, + required final SpotubeMediaCompressionType type, + final String? codec, + final double? bitrate, + final int? bitDepth, + final double? sampleRate}) = _$SpotubeAudioSourceStreamObjectImpl; + + factory _SpotubeAudioSourceStreamObject.fromJson(Map json) = + _$SpotubeAudioSourceStreamObjectImpl.fromJson; + + @override + String get url; + @override + String get container; + @override + SpotubeMediaCompressionType get type; + @override + String? get codec; + @override + double? get bitrate; + @override + int? get bitDepth; + @override + double? get sampleRate; + + /// Create a copy of SpotubeAudioSourceStreamObject + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SpotubeAudioSourceStreamObjectImplCopyWith< + _$SpotubeAudioSourceStreamObjectImpl> + get copyWith => throw _privateConstructorUsedError; +} + SpotubeFullAlbumObject _$SpotubeFullAlbumObjectFromJson( Map json) { return _SpotubeFullAlbumObject.fromJson(json); @@ -4499,7 +5995,6 @@ PluginConfiguration _$PluginConfigurationFromJson(Map json) { /// @nodoc mixin _$PluginConfiguration { - PluginType get type => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; String get version => throw _privateConstructorUsedError; @@ -4527,8 +6022,7 @@ abstract class $PluginConfigurationCopyWith<$Res> { _$PluginConfigurationCopyWithImpl<$Res, PluginConfiguration>; @useResult $Res call( - {PluginType type, - String name, + {String name, String description, String version, String author, @@ -4554,7 +6048,6 @@ class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> @pragma('vm:prefer-inline') @override $Res call({ - Object? type = null, Object? name = null, Object? description = null, Object? version = null, @@ -4566,10 +6059,6 @@ class _$PluginConfigurationCopyWithImpl<$Res, $Val extends PluginConfiguration> Object? repository = freezed, }) { return _then(_value.copyWith( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as PluginType, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -4619,8 +6108,7 @@ abstract class _$$PluginConfigurationImplCopyWith<$Res> @override @useResult $Res call( - {PluginType type, - String name, + {String name, String description, String version, String author, @@ -4644,7 +6132,6 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? type = null, Object? name = null, Object? description = null, Object? version = null, @@ -4656,10 +6143,6 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> Object? repository = freezed, }) { return _then(_$PluginConfigurationImpl( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as PluginType, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -4704,8 +6187,7 @@ class __$$PluginConfigurationImplCopyWithImpl<$Res> @JsonSerializable() class _$PluginConfigurationImpl extends _PluginConfiguration { _$PluginConfigurationImpl( - {required this.type, - required this.name, + {required this.name, required this.description, required this.version, required this.author, @@ -4721,8 +6203,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { factory _$PluginConfigurationImpl.fromJson(Map json) => _$$PluginConfigurationImplFromJson(json); - @override - final PluginType type; @override final String name; @override @@ -4758,7 +6238,7 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { @override String toString() { - return 'PluginConfiguration(type: $type, name: $name, description: $description, version: $version, author: $author, entryPoint: $entryPoint, pluginApiVersion: $pluginApiVersion, apis: $apis, abilities: $abilities, repository: $repository)'; + return 'PluginConfiguration(name: $name, description: $description, version: $version, author: $author, entryPoint: $entryPoint, pluginApiVersion: $pluginApiVersion, apis: $apis, abilities: $abilities, repository: $repository)'; } @override @@ -4766,7 +6246,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PluginConfigurationImpl && - (identical(other.type, type) || other.type == type) && (identical(other.name, name) || other.name == name) && (identical(other.description, description) || other.description == description) && @@ -4787,7 +6266,6 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { @override int get hashCode => Object.hash( runtimeType, - type, name, description, version, @@ -4817,8 +6295,7 @@ class _$PluginConfigurationImpl extends _PluginConfiguration { abstract class _PluginConfiguration extends PluginConfiguration { factory _PluginConfiguration( - {required final PluginType type, - required final String name, + {required final String name, required final String description, required final String version, required final String author, @@ -4832,8 +6309,6 @@ abstract class _PluginConfiguration extends PluginConfiguration { factory _PluginConfiguration.fromJson(Map json) = _$PluginConfigurationImpl.fromJson; - @override - PluginType get type; @override String get name; @override diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 6f416330..9c45cb7c 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -6,6 +6,123 @@ part of 'metadata.dart'; // JsonSerializableGenerator // ************************************************************************** +_$SpotubeAudioSourceContainerPresetLossyImpl + _$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) => + _$SpotubeAudioSourceContainerPresetLossyImpl( + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + name: json['name'] as String, + qualities: (json['qualities'] as List) + .map((e) => SpotubeAudioLossyContainerQuality.fromJson( + Map.from(e as Map))) + .toList(), + ); + +Map _$$SpotubeAudioSourceContainerPresetLossyImplToJson( + _$SpotubeAudioSourceContainerPresetLossyImpl instance) => + { + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'name': instance.name, + 'qualities': instance.qualities.map((e) => e.toJson()).toList(), + }; + +const _$SpotubeMediaCompressionTypeEnumMap = { + SpotubeMediaCompressionType.lossy: 'lossy', + SpotubeMediaCompressionType.lossless: 'lossless', +}; + +_$SpotubeAudioSourceContainerPresetLosslessImpl + _$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) => + _$SpotubeAudioSourceContainerPresetLosslessImpl( + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + name: json['name'] as String, + qualities: (json['qualities'] as List) + .map((e) => SpotubeAudioLosslessContainerQuality.fromJson( + Map.from(e as Map))) + .toList(), + ); + +Map _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( + _$SpotubeAudioSourceContainerPresetLosslessImpl instance) => + { + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'name': instance.name, + 'qualities': instance.qualities.map((e) => e.toJson()).toList(), + }; + +_$SpotubeAudioLossyContainerQualityImpl + _$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) => + _$SpotubeAudioLossyContainerQualityImpl( + bitrate: (json['bitrate'] as num).toDouble(), + ); + +Map _$$SpotubeAudioLossyContainerQualityImplToJson( + _$SpotubeAudioLossyContainerQualityImpl instance) => + { + 'bitrate': instance.bitrate, + }; + +_$SpotubeAudioLosslessContainerQualityImpl + _$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) => + _$SpotubeAudioLosslessContainerQualityImpl( + bitDepth: (json['bitDepth'] as num).toInt(), + sampleRate: (json['sampleRate'] as num).toDouble(), + ); + +Map _$$SpotubeAudioLosslessContainerQualityImplToJson( + _$SpotubeAudioLosslessContainerQualityImpl instance) => + { + 'bitDepth': instance.bitDepth, + 'sampleRate': instance.sampleRate, + }; + +_$SpotubeAudioSourceMatchObjectImpl + _$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) => + _$SpotubeAudioSourceMatchObjectImpl( + id: json['id'] as String, + title: json['title'] as String, + artists: (json['artists'] as List) + .map((e) => e as String) + .toList(), + duration: Duration(microseconds: (json['duration'] as num).toInt()), + thumbnail: json['thumbnail'] as String?, + externalUri: json['externalUri'] as String, + ); + +Map _$$SpotubeAudioSourceMatchObjectImplToJson( + _$SpotubeAudioSourceMatchObjectImpl instance) => + { + 'id': instance.id, + 'title': instance.title, + 'artists': instance.artists, + 'duration': instance.duration.inMicroseconds, + 'thumbnail': instance.thumbnail, + 'externalUri': instance.externalUri, + }; + +_$SpotubeAudioSourceStreamObjectImpl + _$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) => + _$SpotubeAudioSourceStreamObjectImpl( + url: json['url'] as String, + container: json['container'] as String, + type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']), + codec: json['codec'] as String?, + bitrate: (json['bitrate'] as num?)?.toDouble(), + bitDepth: (json['bitDepth'] as num?)?.toInt(), + sampleRate: (json['sampleRate'] as num?)?.toDouble(), + ); + +Map _$$SpotubeAudioSourceStreamObjectImplToJson( + _$SpotubeAudioSourceStreamObjectImpl instance) => + { + 'url': instance.url, + 'container': instance.container, + 'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!, + 'codec': instance.codec, + 'bitrate': instance.bitrate, + 'bitDepth': instance.bitDepth, + 'sampleRate': instance.sampleRate, + }; + _$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) => _$SpotubeFullAlbumObjectImpl( id: json['id'] as String, @@ -419,7 +536,6 @@ Map _$$SpotubeUserObjectImplToJson( _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => _$PluginConfigurationImpl( - type: $enumDecode(_$PluginTypeEnumMap, json['type']), name: json['name'] as String, description: json['description'] as String, version: json['version'] as String, @@ -440,7 +556,6 @@ _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => Map _$$PluginConfigurationImplToJson( _$PluginConfigurationImpl instance) => { - 'type': _$PluginTypeEnumMap[instance.type]!, 'name': instance.name, 'description': instance.description, 'version': instance.version, @@ -453,10 +568,6 @@ Map _$$PluginConfigurationImplToJson( 'repository': instance.repository, }; -const _$PluginTypeEnumMap = { - PluginType.metadata: 'metadata', -}; - const _$PluginApisEnumMap = { PluginApis.webview: 'webview', PluginApis.localstorage: 'localstorage', @@ -466,6 +577,8 @@ const _$PluginApisEnumMap = { const _$PluginAbilitiesEnumMap = { PluginAbilities.authentication: 'authentication', PluginAbilities.scrobbling: 'scrobbling', + PluginAbilities.metadata: 'metadata', + PluginAbilities.audioSource: 'audio-source', }; _$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) => diff --git a/lib/models/metadata/plugin.dart b/lib/models/metadata/plugin.dart index ac6bb0b9..6bc84160 100644 --- a/lib/models/metadata/plugin.dart +++ b/lib/models/metadata/plugin.dart @@ -1,17 +1,20 @@ part of 'metadata.dart'; -enum PluginType { metadata } - enum PluginApis { webview, localstorage, timezone } -enum PluginAbilities { authentication, scrobbling } +enum PluginAbilities { + authentication, + scrobbling, + metadata, + @JsonValue('audio-source') + audioSource, +} @freezed class PluginConfiguration with _$PluginConfiguration { const PluginConfiguration._(); factory PluginConfiguration({ - required PluginType type, required String name, required String description, required String version, diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index cf19c1f5..815fc826 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/metadata/errors/exceptions.dart'; @@ -97,7 +98,6 @@ class MetadataPluginNotifier extends AsyncNotifier { final plugin = plugins[i]; final pluginConfig = PluginConfiguration( - type: PluginType.metadata, name: plugin.name, author: plugin.author, description: plugin.description, @@ -447,6 +447,7 @@ final metadataPluginProvider = FutureProvider( final defaultPlugin = await ref.watch( metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig), ); + final youtubeEngine = ref.read(youtubeEngineProvider); if (defaultPlugin == null) { return null; @@ -456,6 +457,10 @@ final metadataPluginProvider = FutureProvider( final pluginByteCode = await pluginsNotifier.getPluginByteCode(defaultPlugin); - return await MetadataPlugin.create(defaultPlugin, pluginByteCode); + return await MetadataPlugin.create( + youtubeEngine, + defaultPlugin, + pluginByteCode, + ); }, ); diff --git a/lib/services/metadata/endpoints/audio_source.dart b/lib/services/metadata/endpoints/audio_source.dart new file mode 100644 index 00000000..3493c112 --- /dev/null +++ b/lib/services/metadata/endpoints/audio_source.dart @@ -0,0 +1,38 @@ +import 'package:hetu_script/hetu_script.dart'; +import 'package:hetu_script/values.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class MetadataPluginAudioSourceEndpoint { + final Hetu hetu; + MetadataPluginAudioSourceEndpoint(this.hetu); + + HTInstance get hetuMetadataAudioSource => + (hetu.fetch("metadataPlugin") as HTInstance).memberGet("audioSource") + as HTInstance; + + List get supportedPresets { + final raw = hetuMetadataAudioSource.memberGet("supportedPresets") as List; + + return raw + .map((e) => SpotubeAudioSourceContainerPreset.fromJson(e)) + .toList(); + } + + Future> matches( + SpotubeFullTrackObject track, + ) async { + final raw = await hetuMetadataAudioSource + .invoke("matches", positionalArgs: [track]) as List; + + return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList(); + } + + Future> streams( + SpotubeAudioSourceMatchObject match, + ) async { + final raw = await hetuMetadataAudioSource + .invoke("streams", positionalArgs: [match]) as List; + + return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList(); + } +} diff --git a/lib/services/metadata/metadata.dart b/lib/services/metadata/metadata.dart index ab2290f6..5860e0d6 100644 --- a/lib/services/metadata/metadata.dart +++ b/lib/services/metadata/metadata.dart @@ -3,7 +3,9 @@ import 'dart:typed_data'; import 'package:auto_route/auto_route.dart'; import 'package:hetu_otp_util/hetu_otp_util.dart'; import 'package:hetu_script/hetu_script.dart'; -import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart'; +import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart' as spotube_plugin; +import 'package:hetu_spotube_plugin/hetu_spotube_plugin.dart' + hide YouTubeEngine; import 'package:hetu_std/hetu_std.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -15,6 +17,7 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/metadata/apis/localstorage.dart'; import 'package:spotube/services/metadata/endpoints/album.dart'; import 'package:spotube/services/metadata/endpoints/artist.dart'; +import 'package:spotube/services/metadata/endpoints/audio_source.dart'; import 'package:spotube/services/metadata/endpoints/auth.dart'; import 'package:spotube/services/metadata/endpoints/browse.dart'; import 'package:spotube/services/metadata/endpoints/playlist.dart'; @@ -22,13 +25,15 @@ import 'package:spotube/services/metadata/endpoints/search.dart'; import 'package:spotube/services/metadata/endpoints/track.dart'; import 'package:spotube/services/metadata/endpoints/core.dart'; import 'package:spotube/services/metadata/endpoints/user.dart'; +import 'package:spotube/services/youtube_engine/youtube_engine.dart'; const defaultMetadataLimit = "20"; class MetadataPlugin { - static final pluginApiVersion = Version.parse("1.0.0"); + static final pluginApiVersion = Version.parse("2.0.0"); static Future create( + YouTubeEngine youtubeEngine, PluginConfiguration config, Uint8List byteCode, ) async { @@ -76,6 +81,58 @@ class MetadataPlugin { ), ); }, + createYoutubeEngine: () { + return spotube_plugin.YouTubeEngine( + search: (query) async { + final result = await youtubeEngine.searchVideos(query); + return result + .map((video) => { + 'id': video.id.value, + 'title': video.title, + 'author': video.author, + 'duration': video.duration?.inSeconds, + 'description': video.description, + 'uploadDate': video.uploadDate?.toIso8601String(), + 'viewCount': video.engagement.viewCount, + 'likeCount': video.engagement.likeCount, + 'isLive': video.isLive, + }) + .toList(); + }, + getVideo: (videoId) async { + final video = await youtubeEngine.getVideo(videoId); + return { + 'id': video.id.value, + 'title': video.title, + 'author': video.author, + 'duration': video.duration?.inSeconds, + 'description': video.description, + 'uploadDate': video.uploadDate?.toIso8601String(), + 'viewCount': video.engagement.viewCount, + 'likeCount': video.engagement.likeCount, + 'isLive': video.isLive, + }; + }, + streamManifest: (videoId) { + return youtubeEngine.getStreamManifest(videoId).then( + (manifest) { + final streams = manifest.audioOnly + .map( + (stream) => { + 'url': stream.url.toString(), + 'quality': stream.qualityLabel, + 'bitrate': stream.bitrate.bitsPerSecond, + 'container': stream.container.name, + 'videoId': stream.videoId, + }, + ) + .toList(); + return streams; + }, + ); + }, + ); + }, ); await HetuStdLoader.loadBytecodeFlutter(hetu); @@ -98,6 +155,7 @@ class MetadataPlugin { late final MetadataAuthEndpoint auth; + late final MetadataPluginAudioSourceEndpoint audioSource; late final MetadataPluginAlbumEndpoint album; late final MetadataPluginArtistEndpoint artist; late final MetadataPluginBrowseEndpoint browse; @@ -110,6 +168,7 @@ class MetadataPlugin { MetadataPlugin._(this.hetu) { auth = MetadataAuthEndpoint(hetu); + audioSource = MetadataPluginAudioSourceEndpoint(hetu); artist = MetadataPluginArtistEndpoint(hetu); album = MetadataPluginAlbumEndpoint(hetu); browse = MetadataPluginBrowseEndpoint(hetu); diff --git a/pubspec.lock b/pubspec.lock index ff0c689c..08757d74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1230,10 +1230,10 @@ packages: description: path: "." ref: main - resolved-ref: "01935a75640092af7947bfb21a497240376f0c83" + resolved-ref: "32828156bc111d147709f8d644804227bbdfe8f1" url: "https://github.com/KRTirtho/hetu_spotube_plugin.git" source: git - version: "0.0.1" + version: "0.0.2" hetu_std: dependency: "direct main" description: @@ -2888,10 +2888,11 @@ packages: youtube_explode_dart: dependency: "direct main" description: - name: youtube_explode_dart - sha256: "9ff345caf8351c59eb1b7560837f761e08d2beaea3b4187637942715a31a6f58" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: caa3023386dbc10e69c99f49f491148094874671 + url: "https://github.com/Coronon/youtube_explode_dart" + source: git version: "2.5.2" yt_dlp_dart: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 3cc1eb05..46273a32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,7 +138,8 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: ^2.5.1 + youtube_explode_dart: + git: https://github.com/Coronon/youtube_explode_dart yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git From f6d9d64b7d316b208dfc7c86e6f2bbdec1ce4719 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 23 Oct 2025 08:57:45 +0600 Subject: [PATCH 16/47] feat(plugins): filter plugins by abilities in plugins page and show abilities as badge --- lib/collections/spotube_icons.dart | 2 +- lib/l10n/app_en.arb | 4 +- lib/l10n/generated/app_localizations.dart | 12 ++-- lib/l10n/generated/app_localizations_ar.dart | 6 +- lib/l10n/generated/app_localizations_bn.dart | 6 +- lib/l10n/generated/app_localizations_ca.dart | 7 +-- lib/l10n/generated/app_localizations_cs.dart | 6 +- lib/l10n/generated/app_localizations_de.dart | 6 +- lib/l10n/generated/app_localizations_en.dart | 6 +- lib/l10n/generated/app_localizations_es.dart | 7 +-- lib/l10n/generated/app_localizations_eu.dart | 6 +- lib/l10n/generated/app_localizations_fa.dart | 6 +- lib/l10n/generated/app_localizations_fi.dart | 6 +- lib/l10n/generated/app_localizations_fr.dart | 7 +-- lib/l10n/generated/app_localizations_hi.dart | 6 +- lib/l10n/generated/app_localizations_id.dart | 6 +- lib/l10n/generated/app_localizations_it.dart | 6 +- lib/l10n/generated/app_localizations_ja.dart | 6 +- lib/l10n/generated/app_localizations_ka.dart | 7 +-- lib/l10n/generated/app_localizations_ko.dart | 6 +- lib/l10n/generated/app_localizations_ne.dart | 6 +- lib/l10n/generated/app_localizations_nl.dart | 6 +- lib/l10n/generated/app_localizations_pl.dart | 6 +- lib/l10n/generated/app_localizations_pt.dart | 6 +- lib/l10n/generated/app_localizations_ru.dart | 6 +- lib/l10n/generated/app_localizations_ta.dart | 6 +- lib/l10n/generated/app_localizations_th.dart | 6 +- lib/l10n/generated/app_localizations_tl.dart | 6 +- lib/l10n/generated/app_localizations_tr.dart | 6 +- lib/l10n/generated/app_localizations_uk.dart | 6 +- lib/l10n/generated/app_localizations_vi.dart | 6 +- lib/l10n/generated/app_localizations_zh.dart | 11 +--- lib/models/metadata/metadata.freezed.dart | 50 +++++++++++++--- lib/models/metadata/metadata.g.dart | 3 + lib/models/metadata/repository.dart | 1 + .../metadata_plugins/installed_plugin.dart | 18 ++++++ .../metadata_plugins/plugin_repository.dart | 11 ++++ lib/pages/settings/metadata_plugins.dart | 41 ++++++++++++- lib/pages/settings/sections/accounts.dart | 4 +- .../metadata_plugin/core/repositories.dart | 1 + untranslated_messages.json | 58 +++++++++++++++++++ 41 files changed, 270 insertions(+), 118 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 21cf4176..99d9ff74 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -135,7 +135,7 @@ abstract class SpotubeIcons { static const list = FeatherIcons.list; static const device = FeatherIcons.smartphone; static const engine = FeatherIcons.server; - static const extensions = FeatherIcons.package; + static const extensions = Icons.extension_rounded; static const message = FeatherIcons.send; static const upload = FeatherIcons.uploadCloud; static const plugin = Icons.extension_outlined; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 833fa724..8c965aa1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -452,14 +452,14 @@ "disclaimer": "Disclaimer", "third_party_plugin_dmca_notice": "The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\nPlease use them at your own risk. For any bugs/issues, please report them to the plugin repository.\n\nIf any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, so we cannot take any action on them.\n\n", "input_does_not_match_format": "Input doesn't match the required format", - "metadata_provider_plugins": "Metadata Provider Plugins", + "plugins": "Plugins", "paste_plugin_download_url": "Paste download url or GitHub/Codeberg repo url or direct link to .smplug file", "download_and_install_plugin_from_url": "Download and install plugin from url", "failed_to_add_plugin_error": "Failed to add plugin: {error}", "upload_plugin_from_file": "Upload plugin from file", "installed": "Installed", "available_plugins": "Available plugins", - "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", + "configure_plugins": "Configure your own metadata provider and audio source plugins", "audio_scrobblers": "Audio Scrobblers", "scrobbling": "Scrobbling", "source": "Source: ", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index d3124728..8d0610ad 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2871,11 +2871,11 @@ abstract class AppLocalizations { /// **'Input doesn\'t match the required format'** String get input_does_not_match_format; - /// No description provided for @metadata_provider_plugins. + /// No description provided for @plugins. /// /// In en, this message translates to: - /// **'Metadata Provider Plugins'** - String get metadata_provider_plugins; + /// **'Plugins'** + String get plugins; /// No description provided for @paste_plugin_download_url. /// @@ -2913,11 +2913,11 @@ abstract class AppLocalizations { /// **'Available plugins'** String get available_plugins; - /// No description provided for @configure_your_own_metadata_plugin. + /// No description provided for @configure_plugins. /// /// In en, this message translates to: - /// **'Configure your own playlist/album/artist/feed metadata provider'** - String get configure_your_own_metadata_plugin; + /// **'Configure your own metadata provider and audio source plugins'** + String get configure_plugins; /// No description provided for @audio_scrobblers. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index b974d2e4..09ec2505 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1504,7 +1504,7 @@ class AppLocalizationsAr extends AppLocalizations { 'المدخل لا يتوافق مع التنسيق المطلوب'; @override - String get metadata_provider_plugins => 'إضافات مزود البيانات'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1529,8 +1529,8 @@ class AppLocalizationsAr extends AppLocalizations { String get available_plugins => 'الإضافات المتوفّرة'; @override - String get configure_your_own_metadata_plugin => - 'تهيئة مزوّد بيانات للقائمة/الألبوم/الفنان/المصدر خاص بك'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'أجهزة تتبع الصوت'; diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index a193c26f..7180a3e5 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1505,7 +1505,7 @@ class AppLocalizationsBn extends AppLocalizations { 'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না'; @override - String get metadata_provider_plugins => 'মেটাডেটা প্রদানকারী প্লাগইনসমূহ'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1530,8 +1530,8 @@ class AppLocalizationsBn extends AppLocalizations { String get available_plugins => 'উপলব্ধ প্লাগইনগুলো'; @override - String get configure_your_own_metadata_plugin => - 'নিজস্ব প্লেলিস্ট/অ্যালবাম/শিল্পী/ফিড মেটাডেটা প্রদানকারী কনফিগার করুন'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স'; diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index 694aa2c7..c9534377 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1514,8 +1514,7 @@ class AppLocalizationsCa extends AppLocalizations { 'L’entrada no coincideix amb el format requerit'; @override - String get metadata_provider_plugins => - 'Complements de proveïdor de metadades'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1539,8 @@ class AppLocalizationsCa extends AppLocalizations { String get available_plugins => 'Complements disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configura el teu propi proveïdor de metadades per llistes/reproduccions àlbum/artista/flux'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers d’àudio'; diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 8ef0e6a9..0a73011c 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1505,7 +1505,7 @@ class AppLocalizationsCs extends AppLocalizations { 'Vstup neodpovídá požadovanému formátu'; @override - String get metadata_provider_plugins => 'Pluginy poskytovatelů metadat'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1530,8 +1530,8 @@ class AppLocalizationsCs extends AppLocalizations { String get available_plugins => 'Dostupné pluginy'; @override - String get configure_your_own_metadata_plugin => - 'Nakonfigurujte si vlastního poskytovatele metadat pro playlist/album/umělec/fid'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio scrobblers'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 870dd76d..de636d0b 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1517,7 +1517,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Eingabe entspricht nicht dem geforderten Format'; @override - String get metadata_provider_plugins => 'Plugins für Metadatenanbieter'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1542,8 +1542,8 @@ class AppLocalizationsDe extends AppLocalizations { String get available_plugins => 'Verfügbare Plugins'; @override - String get configure_your_own_metadata_plugin => - 'Eigenen Anbieter für Playlist-/Album-/Künstler-/Feed-Metadaten konfigurieren'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio-Scrobbler'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 23d379c7..a86718fa 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Input doesn\'t match the required format'; @override - String get metadata_provider_plugins => 'Metadata Provider Plugins'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsEn extends AppLocalizations { String get available_plugins => 'Available plugins'; @override - String get configure_your_own_metadata_plugin => - 'Configure your own playlist/album/artist/feed metadata provider'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio Scrobblers'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index f06c9399..c68ad322 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1517,8 +1517,7 @@ class AppLocalizationsEs extends AppLocalizations { 'La entrada no coincide con el formato requerido'; @override - String get metadata_provider_plugins => - 'Complementos de proveedor de metadatos'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1543,8 +1542,8 @@ class AppLocalizationsEs extends AppLocalizations { String get available_plugins => 'Complementos disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configura tu propio proveedor de metadatos para listas/álbum/artista/feeds'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers de audio'; diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 296c50ed..677d24c5 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1515,7 +1515,7 @@ class AppLocalizationsEu extends AppLocalizations { 'Sarrera ezin da beharrezko formatutik desberdina izan'; @override - String get metadata_provider_plugins => 'Metadaten hornitzailearen pluginak'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1540,8 @@ class AppLocalizationsEu extends AppLocalizations { String get available_plugins => 'Eskaintzen diren pluginak'; @override - String get configure_your_own_metadata_plugin => - 'Konfiguratu zureko playlists-/album-/artista-/feed-metadaten hornitzailea'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audio scrobbler-ak'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index a1203c57..8cda0e00 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsFa extends AppLocalizations { 'ورودی با قالب مورد نیاز تطابق ندارد'; @override - String get metadata_provider_plugins => 'افزونه‌های ارائه‌دهندهٔ متادیتا'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsFa extends AppLocalizations { String get available_plugins => 'افزونه‌های موجود'; @override - String get configure_your_own_metadata_plugin => - 'پیکربندی ارائه‌دهندهٔ متادیتا برای پلی‌لیست/آلبوم/هنرمند/فید به‌صورت سفارشی'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'اسکراب‌بلرهای صوتی'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index b9ee4de6..8342d7ec 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1503,7 +1503,7 @@ class AppLocalizationsFi extends AppLocalizations { String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa'; @override - String get metadata_provider_plugins => 'Metatietojen tarjoajan lisäosat'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1528,8 +1528,8 @@ class AppLocalizationsFi extends AppLocalizations { String get available_plugins => 'Saatavilla olevat lisäosat'; @override - String get configure_your_own_metadata_plugin => - 'Määritä oma soittolistan/albumin/artistin/syötteen metatietojen tarjoaja'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Äänen scrobblerit'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index daee5667..07f42798 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1521,8 +1521,7 @@ class AppLocalizationsFr extends AppLocalizations { 'L\'entrée ne correspond pas au format requis'; @override - String get metadata_provider_plugins => - 'Plugins de fournisseur de métadonnées'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1548,8 +1547,8 @@ class AppLocalizationsFr extends AppLocalizations { String get available_plugins => 'Plugins disponibles'; @override - String get configure_your_own_metadata_plugin => - 'Configurer votre propre fournisseur de métadonnées de playlist/album/artiste/flux'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers audio'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 65279d70..7cd951b4 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1509,7 +1509,7 @@ class AppLocalizationsHi extends AppLocalizations { 'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है'; @override - String get metadata_provider_plugins => 'मेटाडेटा प्रदाता प्लगइन'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1534,8 +1534,8 @@ class AppLocalizationsHi extends AppLocalizations { String get available_plugins => 'उपलब्ध प्लगइन'; @override - String get configure_your_own_metadata_plugin => - 'अपनी खुद की प्लेलिस्ट/एल्बम/कलाकार/फ़ीड मेटाडेटा प्रदाता कॉन्फ़िगर करें'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 1e0b9f9f..ad2edbfe 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsId extends AppLocalizations { 'Masukan tidak cocok dengan format yang diperlukan'; @override - String get metadata_provider_plugins => 'Plugin Penyedia Metadata'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsId extends AppLocalizations { String get available_plugins => 'Plugin yang tersedia'; @override - String get configure_your_own_metadata_plugin => - 'Konfigurasi penyedia metadata playlist/album/artis/feed Anda sendiri'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers Audio'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index f92eae63..5e75ac08 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1510,7 +1510,7 @@ class AppLocalizationsIt extends AppLocalizations { 'L\'input non corrisponde al formato richiesto'; @override - String get metadata_provider_plugins => 'Plugin del provider di metadati'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1535,8 +1535,8 @@ class AppLocalizationsIt extends AppLocalizations { String get available_plugins => 'Plugin disponibili'; @override - String get configure_your_own_metadata_plugin => - 'Configura il tuo provider di metadati per playlist/album/artista/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobbler audio'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 0e3d98ab..a2500d63 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1474,7 +1474,7 @@ class AppLocalizationsJa extends AppLocalizations { String get input_does_not_match_format => '入力が必須フォーマットと一致しません'; @override - String get metadata_provider_plugins => 'メタデータプロバイダープラグイン'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1499,8 +1499,8 @@ class AppLocalizationsJa extends AppLocalizations { String get available_plugins => '利用可能なプラグイン'; @override - String get configure_your_own_metadata_plugin => - '独自のプレイリスト/アルバム/アーティスト/フィードのメタデータプロバイダーを構成'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'オーディオスクロッブラー'; diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 22d3246f..312c9ccd 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1511,8 +1511,7 @@ class AppLocalizationsKa extends AppLocalizations { 'შეყვანა არ ემთხვევა საჭირო ფორმატს'; @override - String get metadata_provider_plugins => - 'მეტამონაცემების პროვაიდერების პლაგინები'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1537,8 +1536,8 @@ class AppLocalizationsKa extends AppLocalizations { String get available_plugins => 'ხელმისაწვდომი პლაგინები'; @override - String get configure_your_own_metadata_plugin => - 'დააყენეთ თქვენი საკუთარი პლეილისტის/ალბომის/არტისტის/ფიდის მეტამონაცემების პროვაიდერი'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'აუდიო სქრობლერები'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 19b7e544..47f9ad68 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1479,7 +1479,7 @@ class AppLocalizationsKo extends AppLocalizations { String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다'; @override - String get metadata_provider_plugins => '메타데이터 제공자 플러그인'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1503,8 +1503,8 @@ class AppLocalizationsKo extends AppLocalizations { String get available_plugins => '사용 가능한 플러그인'; @override - String get configure_your_own_metadata_plugin => - '자신만의 플레이리스트/앨범/아티스트/피드 메타데이터 제공자 구성'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => '오디오 스크로블러'; diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 53bcb184..fde29abc 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1515,7 +1515,7 @@ class AppLocalizationsNe extends AppLocalizations { String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन'; @override - String get metadata_provider_plugins => 'मेटाडेटा प्रदायक प्लगइनहरू'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1540,8 +1540,8 @@ class AppLocalizationsNe extends AppLocalizations { String get available_plugins => 'उपलब्ध प्लगइनहरू'; @override - String get configure_your_own_metadata_plugin => - 'तपाईंको आफ्नै प्लेलिस्ट/एल्बम/कलाकार/फिड मेटाडेटा प्रदायक कन्फिगर गर्नुहोस्'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू'; diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index dd73f907..29cc6c27 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1509,7 +1509,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Invoer komt niet overeen met het vereiste formaat'; @override - String get metadata_provider_plugins => 'Metadata-aanbieder Plugins'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1534,8 +1534,8 @@ class AppLocalizationsNl extends AppLocalizations { String get available_plugins => 'Beschikbare plugins'; @override - String get configure_your_own_metadata_plugin => - 'Configureer uw eigen metadata-aanbieder voor afspeellijst/album/artiest/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Audioscrobblers'; diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 095242bc..32ffe065 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Wprowadzony tekst nie pasuje do wymaganego formatu'; @override - String get metadata_provider_plugins => 'Wtyczki dostawców metadanych'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsPl extends AppLocalizations { String get available_plugins => 'Dostępne wtyczki'; @override - String get configure_your_own_metadata_plugin => - 'Skonfiguruj własnego dostawcę metadanych dla playlisty/albumu/artysty/kanału'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblery audio'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 4b1fd2bc..0df60587 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1508,7 +1508,7 @@ class AppLocalizationsPt extends AppLocalizations { 'A entrada não corresponde ao formato exigido'; @override - String get metadata_provider_plugins => 'Plugins do provedor de metadados'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1533,8 +1533,8 @@ class AppLocalizationsPt extends AppLocalizations { String get available_plugins => 'Plugins disponíveis'; @override - String get configure_your_own_metadata_plugin => - 'Configure seu próprio provedor de metadados de playlist/álbum/artista/feed'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Scrobblers de áudio'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 9cd091e7..9399f981 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Введенные данные не соответствуют требуемому формату'; @override - String get metadata_provider_plugins => 'Плагины поставщика метаданных'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsRu extends AppLocalizations { String get available_plugins => 'Доступные плагины'; @override - String get configure_your_own_metadata_plugin => - 'Настройте свой собственный поставщик метаданных для плейлиста/альбома/артиста/ленты'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Аудио скробблеры'; diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 204832b2..e6c1d29f 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1517,7 +1517,7 @@ class AppLocalizationsTa extends AppLocalizations { 'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை'; @override - String get metadata_provider_plugins => 'மெட்டாடேட்டா வழங்குநர் பிளகின்கள்'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1542,8 +1542,8 @@ class AppLocalizationsTa extends AppLocalizations { String get available_plugins => 'கிடைக்கக்கூடிய பிளகின்கள்'; @override - String get configure_your_own_metadata_plugin => - 'உங்கள் சொந்த பிளேலிஸ்ட்/ஆல்பம்/கலைஞர்/ஊட்ட மெட்டாடேட்டா வழங்குநரை உள்ளமைக்கவும்'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 5ea104a7..aea6f623 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1500,7 +1500,7 @@ class AppLocalizationsTh extends AppLocalizations { String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ'; @override - String get metadata_provider_plugins => 'ปลั๊กอินผู้ให้บริการเมตาดาต้า'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1525,8 +1525,8 @@ class AppLocalizationsTh extends AppLocalizations { String get available_plugins => 'ปลั๊กอินที่มีอยู่'; @override - String get configure_your_own_metadata_plugin => - 'กำหนดค่าผู้ให้บริการเมตาดาต้าเพลย์ลิสต์/อัลบั้ม/ศิลปิน/ฟีดของคุณเอง'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'เครื่อง scrobbler เสียง'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index de1916e5..ff2ae5da 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1518,7 +1518,7 @@ class AppLocalizationsTl extends AppLocalizations { 'Ang input ay hindi tumutugma sa kinakailangang format'; @override - String get metadata_provider_plugins => 'Mga Plugin ng Metadata Provider'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1543,8 +1543,8 @@ class AppLocalizationsTl extends AppLocalizations { String get available_plugins => 'Mga available na plugin'; @override - String get configure_your_own_metadata_plugin => - 'I-configure ang iyong sariling playlist/album/artist/feed metadata provider'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Mga Audio Scrobbler'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index e6740bb3..64f07b6e 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1511,7 +1511,7 @@ class AppLocalizationsTr extends AppLocalizations { String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor'; @override - String get metadata_provider_plugins => 'Meta Veri Sağlayıcısı Eklentileri'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1536,8 +1536,8 @@ class AppLocalizationsTr extends AppLocalizations { String get available_plugins => 'Mevcut eklentiler'; @override - String get configure_your_own_metadata_plugin => - 'Kendi çalma listenizi/albümünüzü/sanatçınızı/akış meta veri sağlayıcınızı yapılandırın'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Ses Scrobbler\'lar'; diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index aba38ac3..59414ca7 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1507,7 +1507,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Введені дані не відповідають необхідному формату'; @override - String get metadata_provider_plugins => 'Плагіни провайдера метаданих'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1532,8 +1532,8 @@ class AppLocalizationsUk extends AppLocalizations { String get available_plugins => 'Доступні плагіни'; @override - String get configure_your_own_metadata_plugin => - 'Налаштуйте свій власний провайдер метаданих для плейлиста/альбому/виконавця/стрічки'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Аудіо скробблери'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index ec449549..0b980dfd 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1513,7 +1513,7 @@ class AppLocalizationsVi extends AppLocalizations { 'Đầu vào không khớp với định dạng yêu cầu'; @override - String get metadata_provider_plugins => 'Plugin Nhà cung cấp siêu dữ liệu'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1538,8 +1538,8 @@ class AppLocalizationsVi extends AppLocalizations { String get available_plugins => 'Các plugin có sẵn'; @override - String get configure_your_own_metadata_plugin => - 'Cấu hình nhà cung cấp siêu dữ liệu danh sách phát/album/nghệ sĩ/nguồn cấp dữ liệu của riêng bạn'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => 'Bộ scrobbler âm thanh'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index c9e18e72..e2845fe3 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1469,7 +1469,7 @@ class AppLocalizationsZh extends AppLocalizations { String get input_does_not_match_format => '输入与所需格式不匹配'; @override - String get metadata_provider_plugins => '元数据提供者插件'; + String get plugins => 'Plugins'; @override String get paste_plugin_download_url => @@ -1493,7 +1493,8 @@ class AppLocalizationsZh extends AppLocalizations { String get available_plugins => '可用插件'; @override - String get configure_your_own_metadata_plugin => '配置您自己的播放列表/专辑/艺人/订阅元数据提供者'; + String get configure_plugins => + 'Configure your own metadata provider and audio source plugins'; @override String get audio_scrobblers => '音频 Scrobblers'; @@ -2976,9 +2977,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get input_does_not_match_format => '輸入不符合所需格式'; - @override - String get metadata_provider_plugins => '中繼資料供應商外掛程式'; - @override String get paste_plugin_download_url => '貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結'; @@ -3000,9 +2998,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get available_plugins => '可用的外掛程式'; - @override - String get configure_your_own_metadata_plugin => '設定您自己的播放清單/專輯/藝人/動態中繼資料供應商'; - @override String get audio_scrobblers => '音訊 Scrobblers'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index 5d4bc695..f54ee379 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -6539,6 +6539,7 @@ mixin _$MetadataPluginRepository { String get owner => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; String get repoUrl => throw _privateConstructorUsedError; + List get topics => throw _privateConstructorUsedError; /// Serializes this MetadataPluginRepository to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -6556,7 +6557,12 @@ abstract class $MetadataPluginRepositoryCopyWith<$Res> { $Res Function(MetadataPluginRepository) then) = _$MetadataPluginRepositoryCopyWithImpl<$Res, MetadataPluginRepository>; @useResult - $Res call({String name, String owner, String description, String repoUrl}); + $Res call( + {String name, + String owner, + String description, + String repoUrl, + List topics}); } /// @nodoc @@ -6579,6 +6585,7 @@ class _$MetadataPluginRepositoryCopyWithImpl<$Res, Object? owner = null, Object? description = null, Object? repoUrl = null, + Object? topics = null, }) { return _then(_value.copyWith( name: null == name @@ -6597,6 +6604,10 @@ class _$MetadataPluginRepositoryCopyWithImpl<$Res, ? _value.repoUrl : repoUrl // ignore: cast_nullable_to_non_nullable as String, + topics: null == topics + ? _value.topics + : topics // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -6610,7 +6621,12 @@ abstract class _$$MetadataPluginRepositoryImplCopyWith<$Res> __$$MetadataPluginRepositoryImplCopyWithImpl<$Res>; @override @useResult - $Res call({String name, String owner, String description, String repoUrl}); + $Res call( + {String name, + String owner, + String description, + String repoUrl, + List topics}); } /// @nodoc @@ -6632,6 +6648,7 @@ class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res> Object? owner = null, Object? description = null, Object? repoUrl = null, + Object? topics = null, }) { return _then(_$MetadataPluginRepositoryImpl( name: null == name @@ -6650,6 +6667,10 @@ class __$$MetadataPluginRepositoryImplCopyWithImpl<$Res> ? _value.repoUrl : repoUrl // ignore: cast_nullable_to_non_nullable as String, + topics: null == topics + ? _value._topics + : topics // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -6661,7 +6682,9 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { {required this.name, required this.owner, required this.description, - required this.repoUrl}); + required this.repoUrl, + required final List topics}) + : _topics = topics; factory _$MetadataPluginRepositoryImpl.fromJson(Map json) => _$$MetadataPluginRepositoryImplFromJson(json); @@ -6674,10 +6697,17 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { final String description; @override final String repoUrl; + final List _topics; + @override + List get topics { + if (_topics is EqualUnmodifiableListView) return _topics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_topics); + } @override String toString() { - return 'MetadataPluginRepository(name: $name, owner: $owner, description: $description, repoUrl: $repoUrl)'; + return 'MetadataPluginRepository(name: $name, owner: $owner, description: $description, repoUrl: $repoUrl, topics: $topics)'; } @override @@ -6689,13 +6719,14 @@ class _$MetadataPluginRepositoryImpl implements _MetadataPluginRepository { (identical(other.owner, owner) || other.owner == owner) && (identical(other.description, description) || other.description == description) && - (identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl)); + (identical(other.repoUrl, repoUrl) || other.repoUrl == repoUrl) && + const DeepCollectionEquality().equals(other._topics, _topics)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, name, owner, description, repoUrl); + int get hashCode => Object.hash(runtimeType, name, owner, description, + repoUrl, const DeepCollectionEquality().hash(_topics)); /// Create a copy of MetadataPluginRepository /// with the given fields replaced by the non-null parameter values. @@ -6719,7 +6750,8 @@ abstract class _MetadataPluginRepository implements MetadataPluginRepository { {required final String name, required final String owner, required final String description, - required final String repoUrl}) = _$MetadataPluginRepositoryImpl; + required final String repoUrl, + required final List topics}) = _$MetadataPluginRepositoryImpl; factory _MetadataPluginRepository.fromJson(Map json) = _$MetadataPluginRepositoryImpl.fromJson; @@ -6732,6 +6764,8 @@ abstract class _MetadataPluginRepository implements MetadataPluginRepository { String get description; @override String get repoUrl; + @override + List get topics; /// Create a copy of MetadataPluginRepository /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 9c45cb7c..7497053c 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -603,6 +603,8 @@ _$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson( owner: json['owner'] as String, description: json['description'] as String, repoUrl: json['repoUrl'] as String, + topics: + (json['topics'] as List).map((e) => e as String).toList(), ); Map _$$MetadataPluginRepositoryImplToJson( @@ -612,4 +614,5 @@ Map _$$MetadataPluginRepositoryImplToJson( 'owner': instance.owner, 'description': instance.description, 'repoUrl': instance.repoUrl, + 'topics': instance.topics, }; diff --git a/lib/models/metadata/repository.dart b/lib/models/metadata/repository.dart index 06151dee..2a83f791 100644 --- a/lib/models/metadata/repository.dart +++ b/lib/models/metadata/repository.dart @@ -7,6 +7,7 @@ class MetadataPluginRepository with _$MetadataPluginRepository { required String owner, required String description, required String repoUrl, + required List topics, }) = _MetadataPluginRepository; factory MetadataPluginRepository.fromJson(Map json) => diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index ea8cbf29..523fb335 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -5,6 +5,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/markdown/markdown.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/modules/metadata_plugins/plugin_repository.dart'; import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/core/support.dart'; @@ -12,6 +13,11 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:url_launcher/url_launcher.dart'; +final validAbilities = { + PluginAbilities.metadata: ("Metadata", SpotubeIcons.album), + PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music), +}; + class MetadataInstalledPluginItem extends HookConsumerWidget { final PluginConfiguration plugin; final bool isDefault; @@ -79,6 +85,18 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { spacing: 8, children: [ Text(plugin.description), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final ability in plugin.abilities) + if (validAbilities.keys.contains(ability)) + SecondaryBadge( + leading: Icon(validAbilities[ability]!.$2), + child: Text(validAbilities[ability]!.$1), + ), + ], + ), if (repoUrl != null) Wrap( spacing: 8, diff --git a/lib/modules/metadata_plugins/plugin_repository.dart b/lib/modules/metadata_plugins/plugin_repository.dart index c303c46b..9bd71f0a 100644 --- a/lib/modules/metadata_plugins/plugin_repository.dart +++ b/lib/modules/metadata_plugins/plugin_repository.dart @@ -11,6 +11,11 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:change_case/change_case.dart'; +final validTopics = { + "spotube-metadata-plugin": ("Metadata", SpotubeIcons.album), + "spotube-audio-source-plugin": ("Audio Source", SpotubeIcons.music), +}; + class MetadataPluginRepositoryItem extends HookConsumerWidget { final MetadataPluginRepository pluginRepo; const MetadataPluginRepositoryItem({ @@ -208,6 +213,12 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget { ), ), ], + for (final topic in pluginRepo.topics) + if (validTopics.keys.contains(topic)) + SecondaryBadge( + leading: Icon(validTopics[topic]!.$2), + child: Text(validTopics[topic]!.$1), + ), SecondaryBadge( leading: host == "github.com" ? const Icon(SpotubeIcons.github) diff --git a/lib/pages/settings/metadata_plugins.dart b/lib/pages/settings/metadata_plugins.dart index 6698a67f..3601a06d 100644 --- a/lib/pages/settings/metadata_plugins.dart +++ b/lib/pages/settings/metadata_plugins.dart @@ -30,6 +30,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final tabState = useState(0); final formKey = useMemoized(() => GlobalKey(), []); final plugins = ref.watch(metadataPluginsProvider); @@ -49,11 +50,30 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { final pluginRepos = pluginReposSnapshot.asData?.value.items ?? []; if (installedPluginIds.isEmpty) return pluginRepos; - return pluginRepos + final availablePlugins = pluginRepos .whereNot((repo) => installedPluginIds.contains(repo.repoUrl)) .toList(); + + if (tabState.value != 0) { + // metadata only plugins + return availablePlugins + .where( + (d) => d.topics.contains( + tabState.value == 1 + ? "spotube-metadata-plugin" + : "spotube-audio-source-plugin", + ), + ) + .toList(); + } + + return availablePlugins; // all plugins }, - [plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value], + [ + plugins.asData?.value.plugins, + pluginReposSnapshot.asData?.value, + tabState.value, + ], ); return SafeArea( @@ -61,7 +81,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { child: Scaffold( headers: [ TitleBar( - title: Text(context.l10n.metadata_provider_plugins), + title: Text(context.l10n.plugins), ) ], child: Padding( @@ -193,6 +213,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ), ), const SliverGap(12), + SliverToBoxAdapter( + child: TabList( + index: tabState.value, + onChanged: (value) { + tabState.value = value; + }, + children: const [ + TabItem(child: Text("All")), + TabItem(child: Text("Metadata")), + TabItem(child: Text("Audio Source")), + ], + ), + ), + const SliverGap(12), if (plugins.asData?.value.plugins.isNotEmpty ?? false) SliverToBoxAdapter( child: Row( @@ -249,6 +283,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { description: "Loading...", repoUrl: "", owner: "", + topics: [], ), ), ); diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index af8e1b80..ca859ada 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -21,8 +21,8 @@ class SettingsAccountSection extends HookConsumerWidget { children: [ ListTile( leading: const Icon(SpotubeIcons.extensions), - title: Text(context.l10n.metadata_provider_plugins), - subtitle: Text(context.l10n.configure_your_own_metadata_plugin), + title: Text(context.l10n.plugins), + subtitle: Text(context.l10n.configure_plugins), onTap: () { context.pushRoute(const SettingsMetadataProviderRoute()); }, diff --git a/lib/provider/metadata_plugin/core/repositories.dart b/lib/provider/metadata_plugin/core/repositories.dart index 55c11ed2..a78f63d9 100644 --- a/lib/provider/metadata_plugin/core/repositories.dart +++ b/lib/provider/metadata_plugin/core/repositories.dart @@ -49,6 +49,7 @@ class MetadataPluginRepositoriesNotifier owner: repo["owner"]["login"] ?? "", description: repo["description"] ?? "", repoUrl: repo["html_url"] ?? "", + topics: repo["topics"].cast() ?? [], ); }).toList(); diff --git a/untranslated_messages.json b/untranslated_messages.json index ba110540..618ddcf8 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,101 +1,135 @@ { "ar": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "bn": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ca": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "cs": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "de": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "es": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "eu": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fa": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "fr": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "hi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "id": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "it": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ja": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ka": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ko": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ne": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" @@ -103,72 +137,96 @@ "nl": [ "audio_source", + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "pl": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "pt": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ru": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "ta": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "th": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "tl": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "tr": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "uk": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "vi": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "zh": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" ], "zh_TW": [ + "plugins", + "configure_plugins", "source", "uncompressed", "dab_music_source_description" From 3bc296cf224dbede75beaf02385a9ebf8cf55e5b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 25 Oct 2025 23:23:27 +0600 Subject: [PATCH 17/47] feat: add setting default audio source support --- drift_schemas/app_db/drift_schema_v9.json | 1 + lib/l10n/app_en.arb | 5 +- lib/l10n/generated/app_localizations.dart | 24 +- lib/l10n/generated/app_localizations_ar.dart | 11 +- lib/l10n/generated/app_localizations_bn.dart | 11 +- lib/l10n/generated/app_localizations_ca.dart | 11 +- lib/l10n/generated/app_localizations_cs.dart | 11 +- lib/l10n/generated/app_localizations_de.dart | 11 +- lib/l10n/generated/app_localizations_en.dart | 11 +- lib/l10n/generated/app_localizations_es.dart | 11 +- lib/l10n/generated/app_localizations_eu.dart | 11 +- lib/l10n/generated/app_localizations_fa.dart | 11 +- lib/l10n/generated/app_localizations_fi.dart | 11 +- lib/l10n/generated/app_localizations_fr.dart | 11 +- lib/l10n/generated/app_localizations_hi.dart | 11 +- lib/l10n/generated/app_localizations_id.dart | 11 +- lib/l10n/generated/app_localizations_it.dart | 11 +- lib/l10n/generated/app_localizations_ja.dart | 11 +- lib/l10n/generated/app_localizations_ka.dart | 11 +- lib/l10n/generated/app_localizations_ko.dart | 11 +- lib/l10n/generated/app_localizations_ne.dart | 11 +- lib/l10n/generated/app_localizations_nl.dart | 11 +- lib/l10n/generated/app_localizations_pl.dart | 11 +- lib/l10n/generated/app_localizations_pt.dart | 11 +- lib/l10n/generated/app_localizations_ru.dart | 11 +- lib/l10n/generated/app_localizations_ta.dart | 11 +- lib/l10n/generated/app_localizations_th.dart | 11 +- lib/l10n/generated/app_localizations_tl.dart | 11 +- lib/l10n/generated/app_localizations_tr.dart | 11 +- lib/l10n/generated/app_localizations_uk.dart | 11 +- lib/l10n/generated/app_localizations_vi.dart | 11 +- lib/l10n/generated/app_localizations_zh.dart | 14 +- lib/main.dart | 6 +- lib/models/database/database.dart | 16 +- lib/models/database/database.g.dart | 377 +- lib/models/database/database.steps.dart | 274 +- .../database/tables/metadata_plugins.dart | 9 +- .../metadata_plugins/installed_plugin.dart | 290 +- .../root/use_global_subscriptions.dart | 2 +- lib/pages/settings/metadata_plugins.dart | 45 +- lib/provider/metadata_plugin/core/auth.dart | 35 +- .../metadata_plugin/core/scrobble.dart | 6 +- .../metadata_plugin/core/support.dart | 10 + .../metadata_plugin_provider.dart | 168 +- .../updater/update_checker.dart | 19 +- test/drift/app_db/generated/schema.dart | 43 +- test/drift/app_db/generated/schema_v1.dart | 2 +- test/drift/app_db/generated/schema_v2.dart | 2 +- test/drift/app_db/generated/schema_v3.dart | 2 +- test/drift/app_db/generated/schema_v4.dart | 2 +- test/drift/app_db/generated/schema_v5.dart | 2 +- test/drift/app_db/generated/schema_v6.dart | 2 +- test/drift/app_db/generated/schema_v7.dart | 2 +- test/drift/app_db/generated/schema_v8.dart | 2 +- test/drift/app_db/generated/schema_v9.dart | 3568 +++++++++++++++++ untranslated_messages.json | 116 + 56 files changed, 4960 insertions(+), 392 deletions(-) create mode 100644 drift_schemas/app_db/drift_schema_v9.json create mode 100644 test/drift/app_db/generated/schema_v9.dart diff --git a/drift_schemas/app_db/drift_schema_v9.json b/drift_schemas/app_db/drift_schema_v9.json new file mode 100644 index 00000000..73af2588 --- /dev/null +++ b/drift_schemas/app_db/drift_schema_v9.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_quality","getter_name":"audioQuality","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceQualities.high.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceQualities.values)","dart_type_name":"SourceQualities"}},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Slate:0xff64748b\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"piped_instance","getter_name":"pipedInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://pipedapi.kavin.rocks\")","default_client_dart":null,"dsl_features":[]},{"name":"invidious_instance","getter_name":"invidiousInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://inv.nadeko.net\")","default_client_dart":null,"dsl_features":[]},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source","getter_name":"audioSource","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(AudioSource.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(AudioSource.values)","dart_type_name":"AudioSource"}},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"stream_music_codec","getter_name":"streamMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.weba.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"download_music_codec","getter_name":"downloadMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.m4a.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_id","getter_name":"sourceId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceType.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceType.values)","dart_type_name":"SourceType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected_for_metadata","getter_name":"selectedForMetadata","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_metadata\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_metadata\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"selected_for_audio_source","getter_name":"selectedForAudioSource","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_audio_source\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_audio_source\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('2.0.0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_id","source_type"]}}]} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8c965aa1..a292105d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -434,7 +434,10 @@ "update_available": "Update available", "supports_scrobbling": "Supports scrobbling", "plugin_scrobbling_info": "This plugin scrobbles your music to generate your listening history.", - "default_plugin": "Default", + "default_metadata_source": "Default metadata source", + "set_default_metadata_source": "Set default metadata source", + "default_audio_source": "Default audio source", + "set_default_audio_source": "Set default audio source", "set_default": "Set default", "support": "Support", "support_plugin_development": "Support plugin development", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 8d0610ad..0ffcffff 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2763,11 +2763,29 @@ abstract class AppLocalizations { /// **'This plugin scrobbles your music to generate your listening history.'** String get plugin_scrobbling_info; - /// No description provided for @default_plugin. + /// No description provided for @default_metadata_source. /// /// In en, this message translates to: - /// **'Default'** - String get default_plugin; + /// **'Default metadata source'** + String get default_metadata_source; + + /// No description provided for @set_default_metadata_source. + /// + /// In en, this message translates to: + /// **'Set default metadata source'** + String get set_default_metadata_source; + + /// No description provided for @default_audio_source. + /// + /// In en, this message translates to: + /// **'Default audio source'** + String get default_audio_source; + + /// No description provided for @set_default_audio_source. + /// + /// In en, this message translates to: + /// **'Set default audio source'** + String get set_default_audio_source; /// No description provided for @set_default. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 09ec2505..05da9c97 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsAr extends AppLocalizations { 'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.'; @override - String get default_plugin => 'الافتراضي'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'تعيين كافتراضي'; diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index 7180a3e5..b11a9f2f 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsBn extends AppLocalizations { 'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।'; @override - String get default_plugin => 'ডিফল্ট'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন'; diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index c9534377..a18d8c38 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsCa extends AppLocalizations { 'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.'; @override - String get default_plugin => 'Predeterminat'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Establir com a predeterminat'; diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 0a73011c..ce5785d4 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -1442,7 +1442,16 @@ class AppLocalizationsCs extends AppLocalizations { 'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.'; @override - String get default_plugin => 'Výchozí'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Nastavit jako výchozí'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index de636d0b..81a67861 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1455,7 +1455,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.'; @override - String get default_plugin => 'Standard'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Als Standard festlegen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index a86718fa..513daa77 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1442,7 +1442,16 @@ class AppLocalizationsEn extends AppLocalizations { 'This plugin scrobbles your music to generate your listening history.'; @override - String get default_plugin => 'Default'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Set default'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index c68ad322..08426481 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1452,7 +1452,16 @@ class AppLocalizationsEs extends AppLocalizations { 'Este complemento scrobblea tu música para generar tu historial de reproducción.'; @override - String get default_plugin => 'Predeterminado'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Establecer como predeterminado'; diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 677d24c5..14b8e01f 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -1451,7 +1451,16 @@ class AppLocalizationsEu extends AppLocalizations { 'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.'; @override - String get default_plugin => 'Lehenetsia'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Lehenetsi gisa ezarri'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 8cda0e00..d0f73246 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1441,7 +1441,16 @@ class AppLocalizationsFa extends AppLocalizations { 'این افزونه موسیقی شما را اسکراب می‌کند تا تاریخچهٔ شنیداری‌تان را تولید کند.'; @override - String get default_plugin => 'پیش‌فرض'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'تنظیم به عنوان پیش‌فرض'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 8342d7ec..751eb0c1 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1443,7 +1443,16 @@ class AppLocalizationsFi extends AppLocalizations { 'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.'; @override - String get default_plugin => 'Oletus'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Aseta oletukseksi'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 07f42798..068701cc 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1457,7 +1457,16 @@ class AppLocalizationsFr extends AppLocalizations { 'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.'; @override - String get default_plugin => 'Par défaut'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Définir par défaut'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 7cd951b4..3c16bdfc 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsHi extends AppLocalizations { 'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।'; @override - String get default_plugin => 'डिफ़ॉल्ट'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'डिफ़ॉल्ट सेट करें'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index ad2edbfe..f1231523 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1449,7 +1449,16 @@ class AppLocalizationsId extends AppLocalizations { 'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.'; @override - String get default_plugin => 'Bawaan'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Atur sebagai bawaan'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 5e75ac08..c781bd31 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsIt extends AppLocalizations { 'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.'; @override - String get default_plugin => 'Predefinito'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Imposta come predefinito'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index a2500d63..525e6c66 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1416,7 +1416,16 @@ class AppLocalizationsJa extends AppLocalizations { String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。'; @override - String get default_plugin => 'デフォルト'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'デフォルトに設定'; diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 312c9ccd..3d960d7e 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsKa extends AppLocalizations { 'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.'; @override - String get default_plugin => 'ნაგულისხმევი'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ნაგულისხმევად დაყენება'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 47f9ad68..12a3a5e3 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1421,7 +1421,16 @@ class AppLocalizationsKo extends AppLocalizations { String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.'; @override - String get default_plugin => '기본'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => '기본값으로 설정'; diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index fde29abc..64b3311e 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -1454,7 +1454,16 @@ class AppLocalizationsNe extends AppLocalizations { 'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।'; @override - String get default_plugin => 'पूर्वनिर्धारित'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्'; diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 29cc6c27..78d34a27 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsNl extends AppLocalizations { 'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.'; @override - String get default_plugin => 'Standaard'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Instellen als standaard'; diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 32ffe065..afe29fca 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -1449,7 +1449,16 @@ class AppLocalizationsPl extends AppLocalizations { 'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.'; @override - String get default_plugin => 'Domyślna'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Ustaw jako domyślną'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 0df60587..74ead955 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsPt extends AppLocalizations { 'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.'; @override - String get default_plugin => 'Padrão'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Definir como padrão'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 9399f981..f6ad081e 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1448,7 +1448,16 @@ class AppLocalizationsRu extends AppLocalizations { 'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.'; @override - String get default_plugin => 'По умолчанию'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Установить по умолчанию'; diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index e6c1d29f..94719207 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -1455,7 +1455,16 @@ class AppLocalizationsTa extends AppLocalizations { 'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.'; @override - String get default_plugin => 'இயல்புநிலை'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'இயல்புநிலையாக அமைக்கவும்'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index aea6f623..11d82dd6 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1440,7 +1440,16 @@ class AppLocalizationsTh extends AppLocalizations { 'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ'; @override - String get default_plugin => 'ค่าเริ่มต้น'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'ตั้งค่าเริ่มต้น'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index ff2ae5da..ac8415f5 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -1456,7 +1456,16 @@ class AppLocalizationsTl extends AppLocalizations { 'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.'; @override - String get default_plugin => 'Default'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Itakda bilang default'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 64f07b6e..cd4f7122 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsTr extends AppLocalizations { 'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.'; @override - String get default_plugin => 'Varsayılan'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Varsayılan olarak ayarla'; diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 59414ca7..a6297b1e 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -1446,7 +1446,16 @@ class AppLocalizationsUk extends AppLocalizations { 'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.'; @override - String get default_plugin => 'За замовчуванням'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Встановити за замовчуванням'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 0b980dfd..1421c907 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1450,7 +1450,16 @@ class AppLocalizationsVi extends AppLocalizations { 'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.'; @override - String get default_plugin => 'Mặc định'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => 'Đặt làm mặc định'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index e2845fe3..6232965a 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1412,7 +1412,16 @@ class AppLocalizationsZh extends AppLocalizations { String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。'; @override - String get default_plugin => '默认'; + String get default_metadata_source => 'Default metadata source'; + + @override + String get set_default_metadata_source => 'Set default metadata source'; + + @override + String get default_audio_source => 'Default audio source'; + + @override + String get set_default_audio_source => 'Set default audio source'; @override String get set_default => '设为默认'; @@ -2920,9 +2929,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。'; - @override - String get default_plugin => '預設'; - @override String get set_default => '設為預設'; diff --git a/lib/main.dart b/lib/main.dart index b5789d6f..f29933e6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,11 +150,13 @@ class Spotube extends HookConsumerWidget { ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); - ref.listen(metadataPluginsProvider, (_, __) {}); - ref.listen(metadataPluginProvider, (_, __) {}); ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); + ref.listen(metadataPluginsProvider, (_, __) {}); + ref.listen(metadataPluginProvider, (_, __) {}); + ref.listen(audioSourcePluginProvider, (_, __) {}); ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {}); + ref.listen(audioSourcePluginUpdateCheckerProvider, (_, __) {}); useFixWindowStretching(); useDisableBatteryOptimizations(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index bc30627d..2df41e9a 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -58,14 +58,14 @@ part 'typeconverters/subtitle.dart'; AudioPlayerStateTable, HistoryTable, LyricsTable, - MetadataPluginsTable, + PluginsTable, ], ) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 8; + int get schemaVersion => 9; @override MigrationStrategy get migration { @@ -199,6 +199,18 @@ class AppDatabase extends _$AppDatabase { } }); }, + from8To9: (m, schema) async { + await m.renameTable(schema.pluginsTable, "metadata_plugins_table"); + await m.renameColumn( + schema.pluginsTable, + "selected", + pluginsTable.selectedForMetadata, + ); + await m.addColumn( + schema.pluginsTable, + pluginsTable.selectedForAudioSource, + ); + }, ), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 8c4def7c..70f6aa26 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3740,12 +3740,12 @@ class LyricsTableCompanion extends UpdateCompanion { } } -class $MetadataPluginsTableTable extends MetadataPluginsTable - with TableInfo<$MetadataPluginsTableTable, MetadataPluginsTableData> { +class $PluginsTableTable extends PluginsTable + with TableInfo<$PluginsTableTable, PluginsTableData> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $MetadataPluginsTableTable(this.attachedDatabase, [this._alias]); + $PluginsTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -3790,24 +3790,32 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable late final GeneratedColumnWithTypeConverter, String> apis = GeneratedColumn('apis', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $MetadataPluginsTableTable.$converterapis); + .withConverter>($PluginsTableTable.$converterapis); @override late final GeneratedColumnWithTypeConverter, String> abilities = GeneratedColumn('abilities', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $MetadataPluginsTableTable.$converterabilities); - static const VerificationMeta _selectedMeta = - const VerificationMeta('selected'); + .withConverter>($PluginsTableTable.$converterabilities); + static const VerificationMeta _selectedForMetadataMeta = + const VerificationMeta('selectedForMetadata'); @override - late final GeneratedColumn selected = GeneratedColumn( - 'selected', aliasedName, false, + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("selected" IN (0, 1))'), + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _selectedForAudioSourceMeta = + const VerificationMeta('selectedForAudioSource'); + @override + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _repositoryMeta = const VerificationMeta('repository'); @override @@ -3821,7 +3829,7 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable 'plugin_api_version', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant('1.0.0')); + defaultValue: const Constant('2.0.0')); @override List get $columns => [ id, @@ -3832,7 +3840,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable entryPoint, apis, abilities, - selected, + selectedForMetadata, + selectedForAudioSource, repository, pluginApiVersion ]; @@ -3840,10 +3849,9 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'metadata_plugins_table'; + static const String $name = 'plugins_table'; @override - VerificationContext validateIntegrity( - Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -3884,9 +3892,17 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable } else if (isInserting) { context.missing(_entryPointMeta); } - if (data.containsKey('selected')) { - context.handle(_selectedMeta, - selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta)); + if (data.containsKey('selected_for_metadata')) { + context.handle( + _selectedForMetadataMeta, + selectedForMetadata.isAcceptableOrUnknown( + data['selected_for_metadata']!, _selectedForMetadataMeta)); + } + if (data.containsKey('selected_for_audio_source')) { + context.handle( + _selectedForAudioSourceMeta, + selectedForAudioSource.isAcceptableOrUnknown( + data['selected_for_audio_source']!, _selectedForAudioSourceMeta)); } if (data.containsKey('repository')) { context.handle( @@ -3906,10 +3922,9 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable @override Set get $primaryKey => {id}; @override - MetadataPluginsTableData map(Map data, - {String? tablePrefix}) { + PluginsTableData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return MetadataPluginsTableData( + return PluginsTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping @@ -3922,14 +3937,17 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable .read(DriftSqlType.string, data['${effectivePrefix}author'])!, entryPoint: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, - apis: $MetadataPluginsTableTable.$converterapis.fromSql(attachedDatabase + apis: $PluginsTableTable.$converterapis.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}apis'])!), - abilities: $MetadataPluginsTableTable.$converterabilities.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!), - selected: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}selected'])!, + abilities: $PluginsTableTable.$converterabilities.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!), + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, repository: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}repository']), pluginApiVersion: attachedDatabase.typeMapping.read( @@ -3938,8 +3956,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable } @override - $MetadataPluginsTableTable createAlias(String alias) { - return $MetadataPluginsTableTable(attachedDatabase, alias); + $PluginsTableTable createAlias(String alias) { + return $PluginsTableTable(attachedDatabase, alias); } static TypeConverter, String> $converterapis = @@ -3948,8 +3966,8 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable const StringListConverter(); } -class MetadataPluginsTableData extends DataClass - implements Insertable { +class PluginsTableData extends DataClass + implements Insertable { final int id; final String name; final String description; @@ -3958,10 +3976,11 @@ class MetadataPluginsTableData extends DataClass final String entryPoint; final List apis; final List abilities; - final bool selected; + final bool selectedForMetadata; + final bool selectedForAudioSource; final String? repository; final String pluginApiVersion; - const MetadataPluginsTableData( + const PluginsTableData( {required this.id, required this.name, required this.description, @@ -3970,7 +3989,8 @@ class MetadataPluginsTableData extends DataClass required this.entryPoint, required this.apis, required this.abilities, - required this.selected, + required this.selectedForMetadata, + required this.selectedForAudioSource, this.repository, required this.pluginApiVersion}); @override @@ -3983,14 +4003,15 @@ class MetadataPluginsTableData extends DataClass map['author'] = Variable(author); map['entry_point'] = Variable(entryPoint); { - map['apis'] = Variable( - $MetadataPluginsTableTable.$converterapis.toSql(apis)); + map['apis'] = + Variable($PluginsTableTable.$converterapis.toSql(apis)); } { map['abilities'] = Variable( - $MetadataPluginsTableTable.$converterabilities.toSql(abilities)); + $PluginsTableTable.$converterabilities.toSql(abilities)); } - map['selected'] = Variable(selected); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); if (!nullToAbsent || repository != null) { map['repository'] = Variable(repository); } @@ -3998,8 +4019,8 @@ class MetadataPluginsTableData extends DataClass return map; } - MetadataPluginsTableCompanion toCompanion(bool nullToAbsent) { - return MetadataPluginsTableCompanion( + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( id: Value(id), name: Value(name), description: Value(description), @@ -4008,7 +4029,8 @@ class MetadataPluginsTableData extends DataClass entryPoint: Value(entryPoint), apis: Value(apis), abilities: Value(abilities), - selected: Value(selected), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), repository: repository == null && nullToAbsent ? const Value.absent() : Value(repository), @@ -4016,10 +4038,10 @@ class MetadataPluginsTableData extends DataClass ); } - factory MetadataPluginsTableData.fromJson(Map json, + factory PluginsTableData.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return MetadataPluginsTableData( + return PluginsTableData( id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), description: serializer.fromJson(json['description']), @@ -4028,7 +4050,10 @@ class MetadataPluginsTableData extends DataClass entryPoint: serializer.fromJson(json['entryPoint']), apis: serializer.fromJson>(json['apis']), abilities: serializer.fromJson>(json['abilities']), - selected: serializer.fromJson(json['selected']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), repository: serializer.fromJson(json['repository']), pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), ); @@ -4045,13 +4070,14 @@ class MetadataPluginsTableData extends DataClass 'entryPoint': serializer.toJson(entryPoint), 'apis': serializer.toJson>(apis), 'abilities': serializer.toJson>(abilities), - 'selected': serializer.toJson(selected), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), 'repository': serializer.toJson(repository), 'pluginApiVersion': serializer.toJson(pluginApiVersion), }; } - MetadataPluginsTableData copyWith( + PluginsTableData copyWith( {int? id, String? name, String? description, @@ -4060,10 +4086,11 @@ class MetadataPluginsTableData extends DataClass String? entryPoint, List? apis, List? abilities, - bool? selected, + bool? selectedForMetadata, + bool? selectedForAudioSource, Value repository = const Value.absent(), String? pluginApiVersion}) => - MetadataPluginsTableData( + PluginsTableData( id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, @@ -4072,13 +4099,14 @@ class MetadataPluginsTableData extends DataClass entryPoint: entryPoint ?? this.entryPoint, apis: apis ?? this.apis, abilities: abilities ?? this.abilities, - selected: selected ?? this.selected, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, repository: repository.present ? repository.value : this.repository, pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, ); - MetadataPluginsTableData copyWithCompanion( - MetadataPluginsTableCompanion data) { - return MetadataPluginsTableData( + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( id: data.id.present ? data.id.value : this.id, name: data.name.present ? data.name.value : this.name, description: @@ -4089,7 +4117,12 @@ class MetadataPluginsTableData extends DataClass data.entryPoint.present ? data.entryPoint.value : this.entryPoint, apis: data.apis.present ? data.apis.value : this.apis, abilities: data.abilities.present ? data.abilities.value : this.abilities, - selected: data.selected.present ? data.selected.value : this.selected, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, repository: data.repository.present ? data.repository.value : this.repository, pluginApiVersion: data.pluginApiVersion.present @@ -4100,7 +4133,7 @@ class MetadataPluginsTableData extends DataClass @override String toString() { - return (StringBuffer('MetadataPluginsTableData(') + return (StringBuffer('PluginsTableData(') ..write('id: $id, ') ..write('name: $name, ') ..write('description: $description, ') @@ -4109,7 +4142,8 @@ class MetadataPluginsTableData extends DataClass ..write('entryPoint: $entryPoint, ') ..write('apis: $apis, ') ..write('abilities: $abilities, ') - ..write('selected: $selected, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') ..write('repository: $repository, ') ..write('pluginApiVersion: $pluginApiVersion') ..write(')')) @@ -4117,12 +4151,23 @@ class MetadataPluginsTableData extends DataClass } @override - int get hashCode => Object.hash(id, name, description, version, author, - entryPoint, apis, abilities, selected, repository, pluginApiVersion); + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); @override bool operator ==(Object other) => identical(this, other) || - (other is MetadataPluginsTableData && + (other is PluginsTableData && other.id == this.id && other.name == this.name && other.description == this.description && @@ -4131,13 +4176,13 @@ class MetadataPluginsTableData extends DataClass other.entryPoint == this.entryPoint && other.apis == this.apis && other.abilities == this.abilities && - other.selected == this.selected && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && other.repository == this.repository && other.pluginApiVersion == this.pluginApiVersion); } -class MetadataPluginsTableCompanion - extends UpdateCompanion { +class PluginsTableCompanion extends UpdateCompanion { final Value id; final Value name; final Value description; @@ -4146,10 +4191,11 @@ class MetadataPluginsTableCompanion final Value entryPoint; final Value> apis; final Value> abilities; - final Value selected; + final Value selectedForMetadata; + final Value selectedForAudioSource; final Value repository; final Value pluginApiVersion; - const MetadataPluginsTableCompanion({ + const PluginsTableCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), this.description = const Value.absent(), @@ -4158,11 +4204,12 @@ class MetadataPluginsTableCompanion this.entryPoint = const Value.absent(), this.apis = const Value.absent(), this.abilities = const Value.absent(), - this.selected = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), this.repository = const Value.absent(), this.pluginApiVersion = const Value.absent(), }); - MetadataPluginsTableCompanion.insert({ + PluginsTableCompanion.insert({ this.id = const Value.absent(), required String name, required String description, @@ -4171,7 +4218,8 @@ class MetadataPluginsTableCompanion required String entryPoint, required List apis, required List abilities, - this.selected = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), this.repository = const Value.absent(), this.pluginApiVersion = const Value.absent(), }) : name = Value(name), @@ -4181,7 +4229,7 @@ class MetadataPluginsTableCompanion entryPoint = Value(entryPoint), apis = Value(apis), abilities = Value(abilities); - static Insertable custom({ + static Insertable custom({ Expression? id, Expression? name, Expression? description, @@ -4190,7 +4238,8 @@ class MetadataPluginsTableCompanion Expression? entryPoint, Expression? apis, Expression? abilities, - Expression? selected, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, Expression? repository, Expression? pluginApiVersion, }) { @@ -4203,13 +4252,16 @@ class MetadataPluginsTableCompanion if (entryPoint != null) 'entry_point': entryPoint, if (apis != null) 'apis': apis, if (abilities != null) 'abilities': abilities, - if (selected != null) 'selected': selected, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, if (repository != null) 'repository': repository, if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, }); } - MetadataPluginsTableCompanion copyWith( + PluginsTableCompanion copyWith( {Value? id, Value? name, Value? description, @@ -4218,10 +4270,11 @@ class MetadataPluginsTableCompanion Value? entryPoint, Value>? apis, Value>? abilities, - Value? selected, + Value? selectedForMetadata, + Value? selectedForAudioSource, Value? repository, Value? pluginApiVersion}) { - return MetadataPluginsTableCompanion( + return PluginsTableCompanion( id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, @@ -4230,7 +4283,9 @@ class MetadataPluginsTableCompanion entryPoint: entryPoint ?? this.entryPoint, apis: apis ?? this.apis, abilities: abilities ?? this.abilities, - selected: selected ?? this.selected, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, repository: repository ?? this.repository, pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, ); @@ -4258,16 +4313,19 @@ class MetadataPluginsTableCompanion map['entry_point'] = Variable(entryPoint.value); } if (apis.present) { - map['apis'] = Variable( - $MetadataPluginsTableTable.$converterapis.toSql(apis.value)); + map['apis'] = + Variable($PluginsTableTable.$converterapis.toSql(apis.value)); } if (abilities.present) { - map['abilities'] = Variable($MetadataPluginsTableTable - .$converterabilities - .toSql(abilities.value)); + map['abilities'] = Variable( + $PluginsTableTable.$converterabilities.toSql(abilities.value)); } - if (selected.present) { - map['selected'] = Variable(selected.value); + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); } if (repository.present) { map['repository'] = Variable(repository.value); @@ -4280,7 +4338,7 @@ class MetadataPluginsTableCompanion @override String toString() { - return (StringBuffer('MetadataPluginsTableCompanion(') + return (StringBuffer('PluginsTableCompanion(') ..write('id: $id, ') ..write('name: $name, ') ..write('description: $description, ') @@ -4289,7 +4347,8 @@ class MetadataPluginsTableCompanion ..write('entryPoint: $entryPoint, ') ..write('apis: $apis, ') ..write('abilities: $abilities, ') - ..write('selected: $selected, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') ..write('repository: $repository, ') ..write('pluginApiVersion: $pluginApiVersion') ..write(')')) @@ -4314,8 +4373,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { $AudioPlayerStateTableTable(this); late final $HistoryTableTable historyTable = $HistoryTableTable(this); late final $LyricsTableTable lyricsTable = $LyricsTableTable(this); - late final $MetadataPluginsTableTable metadataPluginsTable = - $MetadataPluginsTableTable(this); + late final $PluginsTableTable pluginsTable = $PluginsTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', @@ -4334,7 +4392,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { audioPlayerStateTable, historyTable, lyricsTable, - metadataPluginsTable, + pluginsTable, uniqueBlacklist, uniqTrackMatch ]; @@ -6280,8 +6338,8 @@ typedef $$LyricsTableTableProcessedTableManager = ProcessedTableManager< ), LyricsTableData, PrefetchHooks Function()>; -typedef $$MetadataPluginsTableTableCreateCompanionBuilder - = MetadataPluginsTableCompanion Function({ +typedef $$PluginsTableTableCreateCompanionBuilder = PluginsTableCompanion + Function({ Value id, required String name, required String description, @@ -6290,12 +6348,13 @@ typedef $$MetadataPluginsTableTableCreateCompanionBuilder required String entryPoint, required List apis, required List abilities, - Value selected, + Value selectedForMetadata, + Value selectedForAudioSource, Value repository, Value pluginApiVersion, }); -typedef $$MetadataPluginsTableTableUpdateCompanionBuilder - = MetadataPluginsTableCompanion Function({ +typedef $$PluginsTableTableUpdateCompanionBuilder = PluginsTableCompanion + Function({ Value id, Value name, Value description, @@ -6304,14 +6363,15 @@ typedef $$MetadataPluginsTableTableUpdateCompanionBuilder Value entryPoint, Value> apis, Value> abilities, - Value selected, + Value selectedForMetadata, + Value selectedForAudioSource, Value repository, Value pluginApiVersion, }); -class $$MetadataPluginsTableTableFilterComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableFilterComposer({ +class $$PluginsTableTableFilterComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6346,8 +6406,13 @@ class $$MetadataPluginsTableTableFilterComposer column: $table.abilities, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get selected => $composableBuilder( - column: $table.selected, builder: (column) => ColumnFilters(column)); + ColumnFilters get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, + builder: (column) => ColumnFilters(column)); ColumnFilters get repository => $composableBuilder( column: $table.repository, builder: (column) => ColumnFilters(column)); @@ -6357,9 +6422,9 @@ class $$MetadataPluginsTableTableFilterComposer builder: (column) => ColumnFilters(column)); } -class $$MetadataPluginsTableTableOrderingComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableOrderingComposer({ +class $$PluginsTableTableOrderingComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6390,8 +6455,13 @@ class $$MetadataPluginsTableTableOrderingComposer ColumnOrderings get abilities => $composableBuilder( column: $table.abilities, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get selected => $composableBuilder( - column: $table.selected, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get repository => $composableBuilder( column: $table.repository, builder: (column) => ColumnOrderings(column)); @@ -6401,9 +6471,9 @@ class $$MetadataPluginsTableTableOrderingComposer builder: (column) => ColumnOrderings(column)); } -class $$MetadataPluginsTableTableAnnotationComposer - extends Composer<_$AppDatabase, $MetadataPluginsTableTable> { - $$MetadataPluginsTableTableAnnotationComposer({ +class $$PluginsTableTableAnnotationComposer + extends Composer<_$AppDatabase, $PluginsTableTable> { + $$PluginsTableTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -6434,8 +6504,11 @@ class $$MetadataPluginsTableTableAnnotationComposer GeneratedColumnWithTypeConverter, String> get abilities => $composableBuilder(column: $table.abilities, builder: (column) => column); - GeneratedColumn get selected => - $composableBuilder(column: $table.selected, builder: (column) => column); + GeneratedColumn get selectedForMetadata => $composableBuilder( + column: $table.selectedForMetadata, builder: (column) => column); + + GeneratedColumn get selectedForAudioSource => $composableBuilder( + column: $table.selectedForAudioSource, builder: (column) => column); GeneratedColumn get repository => $composableBuilder( column: $table.repository, builder: (column) => column); @@ -6444,35 +6517,31 @@ class $$MetadataPluginsTableTableAnnotationComposer column: $table.pluginApiVersion, builder: (column) => column); } -class $$MetadataPluginsTableTableTableManager extends RootTableManager< +class $$PluginsTableTableTableManager extends RootTableManager< _$AppDatabase, - $MetadataPluginsTableTable, - MetadataPluginsTableData, - $$MetadataPluginsTableTableFilterComposer, - $$MetadataPluginsTableTableOrderingComposer, - $$MetadataPluginsTableTableAnnotationComposer, - $$MetadataPluginsTableTableCreateCompanionBuilder, - $$MetadataPluginsTableTableUpdateCompanionBuilder, + $PluginsTableTable, + PluginsTableData, + $$PluginsTableTableFilterComposer, + $$PluginsTableTableOrderingComposer, + $$PluginsTableTableAnnotationComposer, + $$PluginsTableTableCreateCompanionBuilder, + $$PluginsTableTableUpdateCompanionBuilder, ( - MetadataPluginsTableData, - BaseReferences<_$AppDatabase, $MetadataPluginsTableTable, - MetadataPluginsTableData> + PluginsTableData, + BaseReferences<_$AppDatabase, $PluginsTableTable, PluginsTableData> ), - MetadataPluginsTableData, + PluginsTableData, PrefetchHooks Function()> { - $$MetadataPluginsTableTableTableManager( - _$AppDatabase db, $MetadataPluginsTableTable table) + $$PluginsTableTableTableManager(_$AppDatabase db, $PluginsTableTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$MetadataPluginsTableTableFilterComposer($db: db, $table: table), + $$PluginsTableTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$MetadataPluginsTableTableOrderingComposer( - $db: db, $table: table), + $$PluginsTableTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$MetadataPluginsTableTableAnnotationComposer( - $db: db, $table: table), + $$PluginsTableTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value name = const Value.absent(), @@ -6482,11 +6551,12 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< Value entryPoint = const Value.absent(), Value> apis = const Value.absent(), Value> abilities = const Value.absent(), - Value selected = const Value.absent(), + Value selectedForMetadata = const Value.absent(), + Value selectedForAudioSource = const Value.absent(), Value repository = const Value.absent(), Value pluginApiVersion = const Value.absent(), }) => - MetadataPluginsTableCompanion( + PluginsTableCompanion( id: id, name: name, description: description, @@ -6495,7 +6565,8 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< entryPoint: entryPoint, apis: apis, abilities: abilities, - selected: selected, + selectedForMetadata: selectedForMetadata, + selectedForAudioSource: selectedForAudioSource, repository: repository, pluginApiVersion: pluginApiVersion, ), @@ -6508,11 +6579,12 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< required String entryPoint, required List apis, required List abilities, - Value selected = const Value.absent(), + Value selectedForMetadata = const Value.absent(), + Value selectedForAudioSource = const Value.absent(), Value repository = const Value.absent(), Value pluginApiVersion = const Value.absent(), }) => - MetadataPluginsTableCompanion.insert( + PluginsTableCompanion.insert( id: id, name: name, description: description, @@ -6521,7 +6593,8 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< entryPoint: entryPoint, apis: apis, abilities: abilities, - selected: selected, + selectedForMetadata: selectedForMetadata, + selectedForAudioSource: selectedForAudioSource, repository: repository, pluginApiVersion: pluginApiVersion, ), @@ -6532,23 +6605,21 @@ class $$MetadataPluginsTableTableTableManager extends RootTableManager< )); } -typedef $$MetadataPluginsTableTableProcessedTableManager - = ProcessedTableManager< - _$AppDatabase, - $MetadataPluginsTableTable, - MetadataPluginsTableData, - $$MetadataPluginsTableTableFilterComposer, - $$MetadataPluginsTableTableOrderingComposer, - $$MetadataPluginsTableTableAnnotationComposer, - $$MetadataPluginsTableTableCreateCompanionBuilder, - $$MetadataPluginsTableTableUpdateCompanionBuilder, - ( - MetadataPluginsTableData, - BaseReferences<_$AppDatabase, $MetadataPluginsTableTable, - MetadataPluginsTableData> - ), - MetadataPluginsTableData, - PrefetchHooks Function()>; +typedef $$PluginsTableTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $PluginsTableTable, + PluginsTableData, + $$PluginsTableTableFilterComposer, + $$PluginsTableTableOrderingComposer, + $$PluginsTableTableAnnotationComposer, + $$PluginsTableTableCreateCompanionBuilder, + $$PluginsTableTableUpdateCompanionBuilder, + ( + PluginsTableData, + BaseReferences<_$AppDatabase, $PluginsTableTable, PluginsTableData> + ), + PluginsTableData, + PrefetchHooks Function()>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -6571,6 +6642,6 @@ class $AppDatabaseManager { $$HistoryTableTableTableManager(_db, _db.historyTable); $$LyricsTableTableTableManager get lyricsTable => $$LyricsTableTableTableManager(_db, _db.lyricsTable); - $$MetadataPluginsTableTableTableManager get metadataPluginsTable => - $$MetadataPluginsTableTableTableManager(_db, _db.metadataPluginsTable); + $$PluginsTableTableTableManager get pluginsTable => + $$PluginsTableTableTableManager(_db, _db.pluginsTable); } diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index a228f5a7..babe71b9 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -1,3 +1,4 @@ +// dart format width=80 import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import @@ -1407,7 +1408,7 @@ final class Schema5 extends i0.VersionedSchema { i1.GeneratedColumn _column_55(String aliasedName) => i1.GeneratedColumn('accent_color_scheme', aliasedName, false, type: i1.DriftSqlType.string, - defaultValue: const Constant("Slate:0xff64748b")); + defaultValue: const Constant("Orange:0xFFf97315")); final class Schema6 extends i0.VersionedSchema { Schema6({required super.database}) : super(version: 6); @@ -2053,7 +2054,7 @@ final class Schema8 extends i0.VersionedSchema { _column_13, _column_14, _column_15, - _column_55, + _column_69, _column_17, _column_18, _column_19, @@ -2188,7 +2189,7 @@ final class Schema8 extends i0.VersionedSchema { _column_65, _column_66, _column_67, - _column_69, + _column_70, ], attachedDatabase: database, ), @@ -2200,8 +2201,267 @@ final class Schema8 extends i0.VersionedSchema { } i1.GeneratedColumn _column_69(String aliasedName) => + i1.GeneratedColumn('accent_color_scheme', aliasedName, false, + type: i1.DriftSqlType.string, + defaultValue: const Constant("Slate:0xff64748b")); +i1.GeneratedColumn _column_70(String aliasedName) => i1.GeneratedColumn('plugin_api_version', aliasedName, false, type: i1.DriftSqlType.string, defaultValue: const Constant('1.0.0')); + +final class Schema9 extends i0.VersionedSchema { + Schema9({required super.database}) : super(version: 9); + @override + late final List entities = [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch, + ]; + late final Shape0 authenticationTable = Shape0( + source: i0.VersionedTable( + entityName: 'authentication_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 blacklistTable = Shape1( + source: i0.VersionedTable( + entityName: 'blacklist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_4, + _column_5, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape13 preferencesTable = Shape13( + source: i0.VersionedTable( + entityName: 'preferences_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + _column_69, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_24, + _column_25, + _column_26, + _column_54, + _column_27, + _column_28, + _column_29, + _column_30, + _column_31, + _column_56, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 scrobblerTable = Shape3( + source: i0.VersionedTable( + entityName: 'scrobbler_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_33, + _column_34, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 skipSegmentTable = Shape4( + source: i0.VersionedTable( + entityName: 'skip_segment_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_35, + _column_36, + _column_37, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 sourceMatchTable = Shape5( + source: i0.VersionedTable( + entityName: 'source_match_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_38, + _column_39, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 audioPlayerStateTable = Shape14( + source: i0.VersionedTable( + entityName: 'audio_player_state_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_40, + _column_41, + _column_42, + _column_43, + _column_57, + _column_58, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 historyTable = Shape9( + source: i0.VersionedTable( + entityName: 'history_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_50, + _column_51, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 lyricsTable = Shape10( + source: i0.VersionedTable( + entityName: 'lyrics_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 pluginsTable = Shape16( + source: i0.VersionedTable( + entityName: 'plugins_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_59, + _column_60, + _column_61, + _column_62, + _column_63, + _column_64, + _column_65, + _column_71, + _column_72, + _column_67, + _column_73, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index uniqueBlacklist = i1.Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + final i1.Index uniqTrackMatch = i1.Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); +} + +class Shape16 extends i0.VersionedTable { + Shape16({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get version => + columnsByName['version']! as i1.GeneratedColumn; + i1.GeneratedColumn get author => + columnsByName['author']! as i1.GeneratedColumn; + i1.GeneratedColumn get entryPoint => + columnsByName['entry_point']! as i1.GeneratedColumn; + i1.GeneratedColumn get apis => + columnsByName['apis']! as i1.GeneratedColumn; + i1.GeneratedColumn get abilities => + columnsByName['abilities']! as i1.GeneratedColumn; + i1.GeneratedColumn get selectedForMetadata => + columnsByName['selected_for_metadata']! as i1.GeneratedColumn; + i1.GeneratedColumn get selectedForAudioSource => + columnsByName['selected_for_audio_source']! as i1.GeneratedColumn; + i1.GeneratedColumn get repository => + columnsByName['repository']! as i1.GeneratedColumn; + i1.GeneratedColumn get pluginApiVersion => + columnsByName['plugin_api_version']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_71(String aliasedName) => + i1.GeneratedColumn('selected_for_metadata', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); +i1.GeneratedColumn _column_72(String aliasedName) => + i1.GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); +i1.GeneratedColumn _column_73(String aliasedName) => + i1.GeneratedColumn('plugin_api_version', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0')); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -2210,6 +2470,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -2248,6 +2509,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from7To8(migrator, schema); return 8; + case 8: + final schema = Schema9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -2262,6 +2528,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -2272,4 +2539,5 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, )); diff --git a/lib/models/database/tables/metadata_plugins.dart b/lib/models/database/tables/metadata_plugins.dart index 8fa3b064..3447497d 100644 --- a/lib/models/database/tables/metadata_plugins.dart +++ b/lib/models/database/tables/metadata_plugins.dart @@ -1,6 +1,6 @@ part of '../database.dart'; -class MetadataPluginsTable extends Table { +class PluginsTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().withLength(min: 1, max: 50)(); TextColumn get description => text()(); @@ -9,8 +9,11 @@ class MetadataPluginsTable extends Table { TextColumn get entryPoint => text()(); TextColumn get apis => text().map(const StringListConverter())(); TextColumn get abilities => text().map(const StringListConverter())(); - BoolColumn get selected => boolean().withDefault(const Constant(false))(); + BoolColumn get selectedForMetadata => + boolean().withDefault(const Constant(false))(); + BoolColumn get selectedForAudioSource => + boolean().withDefault(const Constant(false))(); TextColumn get repository => text().nullable()(); TextColumn get pluginApiVersion => - text().withDefault(const Constant('1.0.0'))(); + text().withDefault(const Constant('2.0.0'))(); } diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 523fb335..34881aaf 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -3,9 +3,9 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/markdown/markdown.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/modules/metadata_plugins/plugin_repository.dart'; import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/metadata_plugin/core/support.dart'; @@ -20,27 +20,52 @@ final validAbilities = { class MetadataInstalledPluginItem extends HookConsumerWidget { final PluginConfiguration plugin; - final bool isDefault; + final bool isDefaultMetadata; + final bool isDefaultAudioSource; const MetadataInstalledPluginItem({ super.key, required this.plugin, - required this.isDefault, + required this.isDefaultMetadata, + required this.isDefaultAudioSource, }); @override Widget build(BuildContext context, ref) { + final mediaQuery = MediaQuery.sizeOf(context); + final metadataPlugin = ref.watch(metadataPluginProvider); - final isAuthenticatedSnapshot = - ref.watch(metadataPluginAuthenticatedProvider); + final audioSourcePlugin = ref.watch(audioSourcePluginProvider); + final pluginSnapshot = switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataPlugin, + (false, true) => audioSourcePlugin, + _ => null, + }; + final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); - final requiresAuth = - isDefault && plugin.abilities.contains(PluginAbilities.authentication); - final supportsScrobbling = - isDefault && plugin.abilities.contains(PluginAbilities.scrobbling); - final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true; - final updateAvailable = - isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null; - final hasUpdate = isDefault && updateAvailable?.asData?.value != null; + + final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) && + plugin.abilities.contains(PluginAbilities.authentication); + final supportsScrobbling = isDefaultMetadata && + plugin.abilities.contains(PluginAbilities.scrobbling); + + final isMetadataAuthenticatedSnapshot = + ref.watch(metadataPluginAuthenticatedProvider); + final isAudioSourceAuthenticatedSnapshot = + ref.watch(audioSourcePluginAuthenticatedProvider); + final isAuthenticated = + isMetadataAuthenticatedSnapshot.asData?.value == true || + isAudioSourceAuthenticatedSnapshot.asData?.value == true; + + final metadataUpdateAvailable = + ref.watch(metadataPluginUpdateCheckerProvider); + final audioSourceUpdateAvailable = + ref.watch(audioSourcePluginUpdateCheckerProvider); + final updateAvailable = switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataUpdateAvailable, + (false, true) => audioSourceUpdateAvailable, + _ => null, + }; + final hasUpdate = updateAvailable?.asData?.value != null; return Card( child: Column( @@ -218,111 +243,158 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ], ), ), - Row( + Wrap( spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.spaceBetween, children: [ - Button.secondary( - enabled: !isDefault, - onPressed: () async { - await pluginsNotifier.setDefaultPlugin(plugin); - }, - child: Text( - isDefault - ? context.l10n.default_plugin - : context.l10n.set_default, - ), - ), - if (isDefault) - Consumer(builder: (context, ref, _) { - final supportTextSnapshot = - ref.watch(metadataPluginSupportTextProvider); - - if (supportTextSnapshot.hasValue && - supportTextSnapshot.value == null) { - return const SizedBox.shrink(); - } - - final bgColor = context.theme.brightness == Brightness.dark - ? const Color.fromARGB(255, 255, 145, 175) - : Colors.pink[600]; - final textColor = context.theme.brightness == Brightness.dark - ? Colors.pink[700] - : Colors.pink[50]; - - final mediaQuery = MediaQuery.sizeOf(context); - - return Button( - style: ButtonVariance.secondary.copyWith( - decoration: (context, states, value) { - return value.copyWithIfBoxDecoration( - color: bgColor, - ); - }, - textStyle: (context, states, value) { - return value.copyWith( - color: textColor, - ); - }, - iconTheme: (context, states, value) { - return value.copyWith( - color: textColor, - ); + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + if (plugin.abilities.contains(PluginAbilities.metadata)) + Button.secondary( + enabled: !isDefaultMetadata, + onPressed: () async { + await pluginsNotifier.setDefaultMetadataPlugin(plugin); }, + child: Text( + isDefaultMetadata + ? context.l10n.default_metadata_source + : context.l10n.set_default_metadata_source, + ), ), - leading: const Icon(SpotubeIcons.heartFilled), - child: Text(context.l10n.support), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: - Text(context.l10n.support_plugin_development), - content: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: mediaQuery.height * 0.8, - maxWidth: 720, - ), - child: SizedBox( - width: double.infinity, - child: SingleChildScrollView( - child: AppMarkdown( - data: supportTextSnapshot.value ?? "", + if (plugin.abilities.contains(PluginAbilities.audioSource)) + Button.secondary( + enabled: !isDefaultAudioSource, + onPressed: () async { + await pluginsNotifier + .setDefaultAudioSourcePlugin(plugin); + }, + child: Text( + isDefaultAudioSource + ? context.l10n.default_audio_source + : context.l10n.set_default_audio_source, + ), + ), + ], + ), + Row( + mainAxisSize: + mediaQuery.smAndUp ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + spacing: 8, + children: [ + if (isDefaultMetadata || isDefaultAudioSource) + Consumer(builder: (context, ref, _) { + final metadataSupportTextSnapshot = + ref.watch(metadataPluginSupportTextProvider); + final audioSourceSupportTextSnapshot = + ref.watch(audioSourcePluginSupportTextProvider); + + final supportTextSnapshot = + switch ((isDefaultMetadata, isDefaultAudioSource)) { + (true, _) => metadataSupportTextSnapshot, + (false, true) => audioSourceSupportTextSnapshot, + _ => null, + }; + + if ((supportTextSnapshot?.hasValue ?? false) && + supportTextSnapshot?.value == null) { + return const SizedBox.shrink(); + } + + final bgColor = + context.theme.brightness == Brightness.dark + ? const Color.fromARGB(255, 255, 145, 175) + : Colors.pink[600]; + final textColor = + context.theme.brightness == Brightness.dark + ? Colors.pink[700] + : Colors.pink[50]; + + final mediaQuery = MediaQuery.sizeOf(context); + + return Button( + style: ButtonVariance.secondary.copyWith( + decoration: (context, states, value) { + return value.copyWithIfBoxDecoration( + color: bgColor, + ); + }, + textStyle: (context, states, value) { + return value.copyWith( + color: textColor, + ); + }, + iconTheme: (context, states, value) { + return value.copyWith( + color: textColor, + ); + }, + ), + leading: const Icon(SpotubeIcons.heartFilled), + child: Text(context.l10n.support), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + context.l10n.support_plugin_development), + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: mediaQuery.height * 0.8, + maxWidth: 720, + ), + child: SizedBox( + width: double.infinity, + child: SingleChildScrollView( + child: AppMarkdown( + data: supportTextSnapshot + ?.asData?.value ?? + "", + ), + ), ), ), - ), - ), - actions: [ - Button.secondary( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(context.l10n.close), - ), - ], + actions: [ + Button.secondary( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.l10n.close), + ), + ], + ); + }, ); }, ); - }, - ); - }), - const Spacer(), - if (isDefault && requiresAuth && !isAuthenticated) - Button.primary( - onPressed: () async { - await metadataPlugin.asData?.value?.auth.authenticate(); - }, - leading: const Icon(SpotubeIcons.login), - child: Text(context.l10n.login), - ) - else if (isDefault && requiresAuth && isAuthenticated) - Button.destructive( - onPressed: () async { - await metadataPlugin.asData?.value?.auth.logout(); - }, - leading: const Icon(SpotubeIcons.logout), - child: Text(context.l10n.logout), - ) + }), + if ((isDefaultMetadata || isDefaultAudioSource) && + requiresAuth && + !isAuthenticated) + Button.primary( + onPressed: () async { + await pluginSnapshot?.asData?.value?.auth + .authenticate(); + }, + leading: const Icon(SpotubeIcons.login), + child: Text(context.l10n.login), + ) + else if ((isDefaultMetadata || isDefaultAudioSource) && + requiresAuth && + isAuthenticated) + Button.destructive( + onPressed: () async { + await pluginSnapshot?.asData?.value?.auth.logout(); + }, + leading: const Icon(SpotubeIcons.logout), + child: Text(context.l10n.logout), + ), + ], + ) ], ) ], diff --git a/lib/modules/root/use_global_subscriptions.dart b/lib/modules/root/use_global_subscriptions.dart index 68f70b5a..9a492d31 100644 --- a/lib/modules/root/use_global_subscriptions.dart +++ b/lib/modules/root/use_global_subscriptions.dart @@ -31,7 +31,7 @@ void useGlobalSubscriptions(WidgetRef ref) { showDialog( context: context, builder: (context) => MetadataPluginUpdateAvailableDialog( - plugin: pluginConfig.defaultPluginConfig!, + plugin: pluginConfig.defaultMetadataPluginConfig!, update: pluginUpdate, ), ); diff --git a/lib/pages/settings/metadata_plugins.dart b/lib/pages/settings/metadata_plugins.dart index 3601a06d..d4cb1ecf 100644 --- a/lib/pages/settings/metadata_plugins.dart +++ b/lib/pages/settings/metadata_plugins.dart @@ -56,15 +56,15 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { if (tabState.value != 0) { // metadata only plugins - return availablePlugins - .where( - (d) => d.topics.contains( - tabState.value == 1 - ? "spotube-metadata-plugin" - : "spotube-audio-source-plugin", - ), - ) - .toList(); + return availablePlugins.where( + (d) { + return d.topics.contains( + tabState.value == 1 + ? "spotube-metadata-plugin" + : "spotube-audio-source-plugin", + ); + }, + ).toList(); } return availablePlugins; // all plugins @@ -76,6 +76,18 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ], ); + final installedPlugins = useMemoized?>(() { + if (tabState.value == 0) return plugins.asData?.value.plugins; + + return plugins.asData?.value.plugins.where((d) { + return d.abilities.contains( + tabState.value == 1 + ? PluginAbilities.metadata + : PluginAbilities.audioSource, + ); + }).toList(); + }, [tabState.value, plugins.asData?.value]); + return SafeArea( bottom: false, child: Scaffold( @@ -241,15 +253,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget { ), const SliverGap(20), SliverList.separated( - itemCount: plugins.asData?.value.plugins.length ?? 0, + itemCount: installedPlugins?.length ?? 0, separatorBuilder: (context, index) => const Gap(12), itemBuilder: (context, index) { - final plugin = plugins.asData!.value.plugins[index]; - final isDefault = - plugins.asData!.value.defaultPlugin == index; + final plugin = installedPlugins![index]; + final isDefaultMetadata = + plugins.asData!.value.defaultMetadataPluginConfig?.slug == + plugin.slug; + final isDefaultAudioSource = plugins + .asData!.value.defaultAudioSourcePluginConfig?.slug == + plugin.slug; return MetadataInstalledPluginItem( plugin: plugin, - isDefault: isDefault, + isDefaultMetadata: isDefaultMetadata, + isDefaultAudioSource: isDefaultAudioSource, ); }, ), diff --git a/lib/provider/metadata_plugin/core/auth.dart b/lib/provider/metadata_plugin/core/auth.dart index 9aa696fc..647b94f9 100644 --- a/lib/provider/metadata_plugin/core/auth.dart +++ b/lib/provider/metadata_plugin/core/auth.dart @@ -8,7 +8,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier { @override FutureOr build() async { final defaultPluginConfig = ref.watch(metadataPluginsProvider); - if (defaultPluginConfig.asData?.value.defaultPluginConfig?.abilities + if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities .contains(PluginAbilities.authentication) != true) { return false; @@ -35,3 +35,36 @@ final metadataPluginAuthenticatedProvider = AsyncNotifierProvider( MetadataPluginAuthenticatedNotifier.new, ); + +class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier { + @override + FutureOr build() async { + final defaultPluginConfig = ref.watch(metadataPluginsProvider); + if (defaultPluginConfig + .asData?.value.defaultAudioSourcePluginConfig?.abilities + .contains(PluginAbilities.authentication) != + true) { + return false; + } + + final defaultPlugin = await ref.watch(audioSourcePluginProvider.future); + if (defaultPlugin == null) { + return false; + } + + final sub = defaultPlugin.auth.authStateStream.listen((event) { + state = AsyncData(defaultPlugin.auth.isAuthenticated()); + }); + + ref.onDispose(() { + sub.cancel(); + }); + + return defaultPlugin.auth.isAuthenticated(); + } +} + +final audioSourcePluginAuthenticatedProvider = + AsyncNotifierProvider( + MetadataPluginAuthenticatedNotifier.new, +); diff --git a/lib/provider/metadata_plugin/core/scrobble.dart b/lib/provider/metadata_plugin/core/scrobble.dart index 376572ad..0f8fcc19 100644 --- a/lib/provider/metadata_plugin/core/scrobble.dart +++ b/lib/provider/metadata_plugin/core/scrobble.dart @@ -10,8 +10,10 @@ class MetadataPluginScrobbleNotifier @override build() { final metadataPlugin = ref.watch(metadataPluginProvider); - final pluginConfig = - ref.watch(metadataPluginsProvider).valueOrNull?.defaultPluginConfig; + final pluginConfig = ref + .watch(metadataPluginsProvider) + .valueOrNull + ?.defaultMetadataPluginConfig; if (metadataPlugin.valueOrNull == null || pluginConfig == null || diff --git a/lib/provider/metadata_plugin/core/support.dart b/lib/provider/metadata_plugin/core/support.dart index 88bfbf5c..8864f1b1 100644 --- a/lib/provider/metadata_plugin/core/support.dart +++ b/lib/provider/metadata_plugin/core/support.dart @@ -9,3 +9,13 @@ final metadataPluginSupportTextProvider = FutureProvider((ref) async { } return await metadataPlugin.core.support; }); + +final audioSourcePluginSupportTextProvider = + FutureProvider((ref) async { + final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future); + + if (audioSourcePlugin == null) { + throw 'No metadata plugin available'; + } + return await audioSourcePlugin.core.support; +}); diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index 815fc826..13d72c93 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -25,18 +25,28 @@ final allowedDomainsRegex = RegExp( class MetadataPluginState { final List plugins; - final int defaultPlugin; + final int defaultMetadataPlugin; + final int defaultAudioSourcePlugin; const MetadataPluginState({ this.plugins = const [], - this.defaultPlugin = -1, + this.defaultMetadataPlugin = -1, + this.defaultAudioSourcePlugin = -1, }); - PluginConfiguration? get defaultPluginConfig { - if (defaultPlugin < 0 || defaultPlugin >= plugins.length) { + PluginConfiguration? get defaultMetadataPluginConfig { + if (defaultMetadataPlugin < 0 || defaultMetadataPlugin >= plugins.length) { return null; } - return plugins[defaultPlugin]; + return plugins[defaultMetadataPlugin]; + } + + PluginConfiguration? get defaultAudioSourcePluginConfig { + if (defaultAudioSourcePlugin < 0 || + defaultAudioSourcePlugin >= plugins.length) { + return null; + } + return plugins[defaultAudioSourcePlugin]; } factory MetadataPluginState.fromJson(Map json) { @@ -44,24 +54,30 @@ class MetadataPluginState { plugins: (json["plugins"] as List) .map((e) => PluginConfiguration.fromJson(e)) .toList(), - defaultPlugin: json["default_plugin"] ?? -1, + defaultMetadataPlugin: json["default_metadata_plugin"] ?? -1, + defaultAudioSourcePlugin: json['default_audio_source_plugin'], ); } Map toJson() { return { "plugins": plugins.map((e) => e.toJson()).toList(), - "default_plugin": defaultPlugin, + "default_metadata_plugin": defaultMetadataPlugin, + "default_audio_source_plugin": defaultAudioSourcePlugin }; } MetadataPluginState copyWith({ List? plugins, - int? defaultPlugin, + int? defaultMetadataPlugin, + int? defaultAudioSourcePlugin, }) { return MetadataPluginState( plugins: plugins ?? this.plugins, - defaultPlugin: defaultPlugin ?? this.defaultPlugin, + defaultMetadataPlugin: + defaultMetadataPlugin ?? this.defaultMetadataPlugin, + defaultAudioSourcePlugin: + defaultAudioSourcePlugin ?? this.defaultAudioSourcePlugin, ); } } @@ -73,7 +89,7 @@ class MetadataPluginNotifier extends AsyncNotifier { build() async { final database = ref.watch(databaseProvider); - final subscription = database.metadataPluginsTable.select().watch().listen( + final subscription = database.pluginsTable.select().watch().listen( (event) async { state = AsyncValue.data(await toStatePlugins(event)); }, @@ -83,15 +99,16 @@ class MetadataPluginNotifier extends AsyncNotifier { subscription.cancel(); }); - final plugins = await database.metadataPluginsTable.select().get(); + final plugins = await database.pluginsTable.select().get(); return await toStatePlugins(plugins); } Future toStatePlugins( - List plugins, + List plugins, ) async { - int defaultPlugin = -1; + int defaultMetadataPlugin = -1; + int defaultAudioSourcePlugin = -1; final pluginConfigs = []; for (int i = 0; i < plugins.length; i++) { @@ -133,20 +150,24 @@ class MetadataPluginNotifier extends AsyncNotifier { !await pluginJsonFile.exists() || !await pluginBinaryFile.exists()) { // Delete the plugin entry from DB if the plugin files are not there. - await database.metadataPluginsTable.deleteOne(plugin); + await database.pluginsTable.deleteOne(plugin); continue; } pluginConfigs.add(pluginConfig); - if (plugin.selected) { - defaultPlugin = pluginConfigs.length - 1; + if (plugin.selectedForMetadata) { + defaultMetadataPlugin = pluginConfigs.length - 1; + } + if (plugin.selectedForAudioSource) { + defaultAudioSourcePlugin = pluginConfigs.length - 1; } } return MetadataPluginState( plugins: pluginConfigs, - defaultPlugin: defaultPlugin, + defaultMetadataPlugin: defaultMetadataPlugin, + defaultAudioSourcePlugin: defaultAudioSourcePlugin, ); } @@ -327,7 +348,7 @@ class MetadataPluginNotifier extends AsyncNotifier { Future addPlugin(PluginConfiguration plugin) async { _assertPluginApiCompatibility(plugin); - final pluginRes = await (database.metadataPluginsTable.select() + final pluginRes = await (database.pluginsTable.select() ..where( (tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author), @@ -339,8 +360,8 @@ class MetadataPluginNotifier extends AsyncNotifier { throw MetadataPluginException.duplicatePlugin(); } - await database.metadataPluginsTable.insertOne( - MetadataPluginsTableCompanion.insert( + await database.pluginsTable.insertOne( + PluginsTableCompanion.insert( name: plugin.name, author: plugin.author, description: plugin.description, @@ -351,7 +372,14 @@ class MetadataPluginNotifier extends AsyncNotifier { pluginApiVersion: Value(plugin.pluginApiVersion), repository: Value(plugin.repository), // Setting the very first plugin as the default plugin - selected: Value(state.valueOrNull?.plugins.isEmpty ?? true), + selectedForMetadata: Value( + (state.valueOrNull?.plugins.isEmpty ?? true) && + plugin.abilities.contains(PluginAbilities.metadata), + ), + selectedForAudioSource: Value( + (state.valueOrNull?.plugins.isEmpty ?? true) && + plugin.abilities.contains(PluginAbilities.audioSource), + ), ), ); } @@ -362,17 +390,32 @@ class MetadataPluginNotifier extends AsyncNotifier { if (pluginExtractionDir.existsSync()) { await pluginExtractionDir.delete(recursive: true); } - await database.metadataPluginsTable.deleteWhere((tbl) => + await database.pluginsTable.deleteWhere((tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)); // Same here, if the removed plugin is the default plugin // set the first available plugin as the default plugin // only when there is 1 remaining plugin - if (state.valueOrNull?.defaultPluginConfig == plugin) { - final remainingPlugins = - state.valueOrNull?.plugins.where((p) => p != plugin) ?? []; + if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) { + final remainingPlugins = state.valueOrNull?.plugins.where( + (p) => + p != plugin && p.abilities.contains(PluginAbilities.metadata), + ) ?? + []; if (remainingPlugins.length == 1) { - await setDefaultPlugin(remainingPlugins.first); + await setDefaultMetadataPlugin(remainingPlugins.first); + } + } + + if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) { + final remainingPlugins = state.valueOrNull?.plugins.where( + (p) => + p != plugin && + p.abilities.contains(PluginAbilities.audioSource), + ) ?? + []; + if (remainingPlugins.length == 1) { + await setDefaultAudioSourcePlugin(remainingPlugins.first); } } } @@ -381,7 +424,10 @@ class MetadataPluginNotifier extends AsyncNotifier { PluginConfiguration plugin, PluginUpdateAvailable update, ) async { - final isDefault = plugin == state.valueOrNull?.defaultPluginConfig; + final isDefaultMetadata = + plugin == state.valueOrNull?.defaultMetadataPluginConfig; + final isDefaultAudioSource = + plugin == state.valueOrNull?.defaultAudioSourcePluginConfig; final pluginUpdatedConfig = await downloadAndCachePlugin(update.downloadUrl); @@ -394,21 +440,46 @@ class MetadataPluginNotifier extends AsyncNotifier { await removePlugin(plugin); await addPlugin(pluginUpdatedConfig); - if (isDefault) { - await setDefaultPlugin(pluginUpdatedConfig); + if (isDefaultMetadata) { + await setDefaultMetadataPlugin(pluginUpdatedConfig); + } + if (isDefaultAudioSource) { + await setDefaultAudioSourcePlugin(pluginUpdatedConfig); } } - Future setDefaultPlugin(PluginConfiguration plugin) async { - await database.metadataPluginsTable - .update() - .write(const MetadataPluginsTableCompanion(selected: Value(false))); + Future setDefaultMetadataPlugin(PluginConfiguration plugin) async { + assert( + plugin.abilities.contains(PluginAbilities.metadata), + "Must be a metadata plugin", + ); - await (database.metadataPluginsTable.update() + await database.pluginsTable + .update() + .write(const PluginsTableCompanion(selectedForMetadata: Value(false))); + + await (database.pluginsTable.update() ..where((tbl) => tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author))) .write( - const MetadataPluginsTableCompanion(selected: Value(true)), + const PluginsTableCompanion(selectedForMetadata: Value(true)), + ); + } + + Future setDefaultAudioSourcePlugin(PluginConfiguration plugin) async { + assert( + plugin.abilities.contains(PluginAbilities.audioSource), + "Must be an audio-source plugin", + ); + + await database.pluginsTable.update().write( + const PluginsTableCompanion(selectedForAudioSource: Value(false))); + + await (database.pluginsTable.update() + ..where((tbl) => + tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author))) + .write( + const PluginsTableCompanion(selectedForAudioSource: Value(true)), ); } @@ -445,7 +516,32 @@ final metadataPluginsProvider = final metadataPluginProvider = FutureProvider( (ref) async { final defaultPlugin = await ref.watch( - metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig), + metadataPluginsProvider + .selectAsync((data) => data.defaultMetadataPluginConfig), + ); + final youtubeEngine = ref.read(youtubeEngineProvider); + + if (defaultPlugin == null) { + return null; + } + + final pluginsNotifier = ref.read(metadataPluginsProvider.notifier); + final pluginByteCode = + await pluginsNotifier.getPluginByteCode(defaultPlugin); + + return await MetadataPlugin.create( + youtubeEngine, + defaultPlugin, + pluginByteCode, + ); + }, +); + +final audioSourcePluginProvider = FutureProvider( + (ref) async { + final defaultPlugin = await ref.watch( + metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig), ); final youtubeEngine = ref.read(youtubeEngineProvider); diff --git a/lib/provider/metadata_plugin/updater/update_checker.dart b/lib/provider/metadata_plugin/updater/update_checker.dart index b53ab2b5..6a7dc589 100644 --- a/lib/provider/metadata_plugin/updater/update_checker.dart +++ b/lib/provider/metadata_plugin/updater/update_checker.dart @@ -8,10 +8,25 @@ final metadataPluginUpdateCheckerProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null || - metadataPluginConfigs.defaultPluginConfig == null) { + metadataPluginConfigs.defaultMetadataPluginConfig == null) { return null; } return metadataPlugin.core - .checkUpdate(metadataPluginConfigs.defaultPluginConfig!); + .checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!); +}); + +final audioSourcePluginUpdateCheckerProvider = + FutureProvider((ref) async { + final audioSourcePluginConfigs = + await ref.watch(metadataPluginsProvider.future); + final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future); + + if (audioSourcePlugin == null || + audioSourcePluginConfigs.defaultAudioSourcePluginConfig == null) { + return null; + } + + return audioSourcePlugin.core + .checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!); }); diff --git a/test/drift/app_db/generated/schema.dart b/test/drift/app_db/generated/schema.dart index dfd3edf3..413b4408 100644 --- a/test/drift/app_db/generated/schema.dart +++ b/test/drift/app_db/generated/schema.dart @@ -1,41 +1,44 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; -import 'schema_v5.dart' as v5; -import 'schema_v4.dart' as v4; -import 'schema_v8.dart' as v8; -import 'schema_v3.dart' as v3; -import 'schema_v2.dart' as v2; import 'schema_v1.dart' as v1; -import 'schema_v7.dart' as v7; +import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; +import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; +import 'schema_v8.dart' as v8; +import 'schema_v9.dart' as v9; class GeneratedHelper implements SchemaInstantiationHelper { @override GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { switch (version) { - case 5: - return v5.DatabaseAtV5(db); - case 4: - return v4.DatabaseAtV4(db); - case 8: - return v8.DatabaseAtV8(db); - case 3: - return v3.DatabaseAtV3(db); - case 2: - return v2.DatabaseAtV2(db); case 1: return v1.DatabaseAtV1(db); - case 7: - return v7.DatabaseAtV7(db); + case 2: + return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + case 4: + return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); case 6: return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); + case 8: + return v8.DatabaseAtV8(db); + case 9: + return v9.DatabaseAtV9(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; } diff --git a/test/drift/app_db/generated/schema_v1.dart b/test/drift/app_db/generated/schema_v1.dart index 7a849d18..ca848561 100644 --- a/test/drift/app_db/generated/schema_v1.dart +++ b/test/drift/app_db/generated/schema_v1.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v2.dart b/test/drift/app_db/generated/schema_v2.dart index 4b28750d..c9642f86 100644 --- a/test/drift/app_db/generated/schema_v2.dart +++ b/test/drift/app_db/generated/schema_v2.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v3.dart b/test/drift/app_db/generated/schema_v3.dart index 7ddf4d2b..f6416823 100644 --- a/test/drift/app_db/generated/schema_v3.dart +++ b/test/drift/app_db/generated/schema_v3.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v4.dart b/test/drift/app_db/generated/schema_v4.dart index c8f07c6e..4206abdb 100644 --- a/test/drift/app_db/generated/schema_v4.dart +++ b/test/drift/app_db/generated/schema_v4.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v5.dart b/test/drift/app_db/generated/schema_v5.dart index 72c48612..4283aa98 100644 --- a/test/drift/app_db/generated/schema_v5.dart +++ b/test/drift/app_db/generated/schema_v5.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v6.dart b/test/drift/app_db/generated/schema_v6.dart index 9e556976..c0ef0442 100644 --- a/test/drift/app_db/generated/schema_v6.dart +++ b/test/drift/app_db/generated/schema_v6.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v7.dart b/test/drift/app_db/generated/schema_v7.dart index b28397ab..b476efbd 100644 --- a/test/drift/app_db/generated/schema_v7.dart +++ b/test/drift/app_db/generated/schema_v7.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v8.dart b/test/drift/app_db/generated/schema_v8.dart index 33fb4dad..7008eaff 100644 --- a/test/drift/app_db/generated/schema_v8.dart +++ b/test/drift/app_db/generated/schema_v8.dart @@ -1,6 +1,6 @@ +// dart format width=80 // GENERATED CODE, DO NOT EDIT BY HAND. // ignore_for_file: type=lint -//@dart=2.12 import 'package:drift/drift.dart'; class AuthenticationTable extends Table diff --git a/test/drift/app_db/generated/schema_v9.dart b/test/drift/app_db/generated/schema_v9.dart new file mode 100644 index 00000000..cde63c2f --- /dev/null +++ b/test/drift/app_db/generated/schema_v9.dart @@ -0,0 +1,3568 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class AuthenticationTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthenticationTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn cookie = GeneratedColumn( + 'cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn accessToken = GeneratedColumn( + 'access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, cookie, accessToken, expiration]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'authentication_table'; + @override + Set get $primaryKey => {id}; + @override + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!, + accessToken: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + + @override + AuthenticationTable createAlias(String alias) { + return AuthenticationTable(attachedDatabase, alias); + } +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final String cookie; + final String accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['cookie'] = Variable(cookie); + map['access_token'] = Variable(accessToken); + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + String? cookie, + String? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) { + return AuthenticationTableData( + id: data.id.present ? data.id.value : this.id, + cookie: data.cookie.present ? data.cookie.value : this.cookie, + accessToken: + data.accessToken.present ? data.accessToken.value : this.accessToken, + expiration: + data.expiration.present ? data.expiration.value : this.expiration, + ); + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, cookie, accessToken, expiration); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required String cookie, + required String accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable(cookie.value); + } + if (accessToken.present) { + map['access_token'] = Variable(accessToken.value); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class BlacklistTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BlacklistTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementType = GeneratedColumn( + 'element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_table'; + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_type'])!, + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + BlacklistTable createAlias(String alias) { + return BlacklistTable(attachedDatabase, alias); + } +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['element_type'] = Variable(elementType); + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: serializer.fromJson(json['elementType']), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson(elementType), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, String? name, String? elementType, String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + BlacklistTableData copyWithCompanion(BlacklistTableCompanion data) { + return BlacklistTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + elementType: + data.elementType.present ? data.elementType.value : this.elementType, + elementId: data.elementId.present ? data.elementId.value : this.elementId, + ); + } + + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable(elementType.value); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + +class PreferencesTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PreferencesTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn audioQuality = GeneratedColumn( + 'audio_quality', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceQualities.high.name)); + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)); + late final GeneratedColumn accentColorScheme = + GeneratedColumn('accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Slate:0xff64748b")); + late final GeneratedColumn layoutMode = GeneratedColumn( + 'layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)); + late final GeneratedColumn locale = GeneratedColumn( + 'locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: + const Constant('{"languageCode":"system","countryCode":"system"}')); + late final GeneratedColumn market = GeneratedColumn( + 'market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)); + late final GeneratedColumn searchMode = GeneratedColumn( + 'search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)); + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn localLibraryLocation = + GeneratedColumn('local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + late final GeneratedColumn invidiousInstance = + GeneratedColumn('invidious_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://inv.nadeko.net")); + late final GeneratedColumn themeMode = GeneratedColumn( + 'theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)); + late final GeneratedColumn audioSource = GeneratedColumn( + 'audio_source', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(AudioSource.youtube.name)); + late final GeneratedColumn youtubeClientEngine = + GeneratedColumn('youtube_client_engine', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); + late final GeneratedColumn streamMusicCodec = GeneratedColumn( + 'stream_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.weba.name)); + late final GeneratedColumn downloadMusicCodec = + GeneratedColumn('download_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.m4a.name)); + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn connectPort = GeneratedColumn( + 'connect_port', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(-1)); + late final GeneratedColumn cacheMusic = GeneratedColumn( + 'cache_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("cache_music" IN (0, 1))'), + defaultValue: const Constant(true)); + @override + List get $columns => [ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSource, + youtubeClientEngine, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioQuality: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_quality'])!, + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}close_behavior'])!, + accentColorScheme: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}accent_color_scheme'])!, + layoutMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}layout_mode'])!, + locale: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!, + market: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!, + searchMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}search_mode'])!, + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!, + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + invidiousInstance: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, + themeMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, + audioSource: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source'])!, + youtubeClientEngine: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}youtube_client_engine'])!, + streamMusicCodec: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}stream_music_codec'])!, + downloadMusicCodec: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_music_codec'])!, + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + connectPort: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}connect_port'])!, + cacheMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}cache_music'])!, + ); + } + + @override + PreferencesTable createAlias(String alias) { + return PreferencesTable(attachedDatabase, alias); + } +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final String audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final String closeBehavior; + final String accentColorScheme; + final String layoutMode; + final String locale; + final String market; + final String searchMode; + final String downloadLocation; + final String localLibraryLocation; + final String pipedInstance; + final String invidiousInstance; + final String themeMode; + final String audioSource; + final String youtubeClientEngine; + final String streamMusicCodec; + final String downloadMusicCodec; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + final int connectPort; + final bool cacheMusic; + const PreferencesTableData( + {required this.id, + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.invidiousInstance, + required this.themeMode, + required this.audioSource, + required this.youtubeClientEngine, + required this.streamMusicCodec, + required this.downloadMusicCodec, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect, + required this.connectPort, + required this.cacheMusic}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['audio_quality'] = Variable(audioQuality); + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + map['close_behavior'] = Variable(closeBehavior); + map['accent_color_scheme'] = Variable(accentColorScheme); + map['layout_mode'] = Variable(layoutMode); + map['locale'] = Variable(locale); + map['market'] = Variable(market); + map['search_mode'] = Variable(searchMode); + map['download_location'] = Variable(downloadLocation); + map['local_library_location'] = Variable(localLibraryLocation); + map['piped_instance'] = Variable(pipedInstance); + map['invidious_instance'] = Variable(invidiousInstance); + map['theme_mode'] = Variable(themeMode); + map['audio_source'] = Variable(audioSource); + map['youtube_client_engine'] = Variable(youtubeClientEngine); + map['stream_music_codec'] = Variable(streamMusicCodec); + map['download_music_codec'] = Variable(downloadMusicCodec); + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + map['connect_port'] = Variable(connectPort); + map['cache_music'] = Variable(cacheMusic); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + audioQuality: Value(audioQuality), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + invidiousInstance: Value(invidiousInstance), + themeMode: Value(themeMode), + audioSource: Value(audioSource), + youtubeClientEngine: Value(youtubeClientEngine), + streamMusicCodec: Value(streamMusicCodec), + downloadMusicCodec: Value(downloadMusicCodec), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + connectPort: Value(connectPort), + cacheMusic: Value(cacheMusic), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + audioQuality: serializer.fromJson(json['audioQuality']), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: serializer.fromJson(json['closeBehavior']), + accentColorScheme: serializer.fromJson(json['accentColorScheme']), + layoutMode: serializer.fromJson(json['layoutMode']), + locale: serializer.fromJson(json['locale']), + market: serializer.fromJson(json['market']), + searchMode: serializer.fromJson(json['searchMode']), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + invidiousInstance: serializer.fromJson(json['invidiousInstance']), + themeMode: serializer.fromJson(json['themeMode']), + audioSource: serializer.fromJson(json['audioSource']), + youtubeClientEngine: + serializer.fromJson(json['youtubeClientEngine']), + streamMusicCodec: serializer.fromJson(json['streamMusicCodec']), + downloadMusicCodec: + serializer.fromJson(json['downloadMusicCodec']), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + connectPort: serializer.fromJson(json['connectPort']), + cacheMusic: serializer.fromJson(json['cacheMusic']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioQuality': serializer.toJson(audioQuality), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson(closeBehavior), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson(layoutMode), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson(market), + 'searchMode': serializer.toJson(searchMode), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': serializer.toJson(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'invidiousInstance': serializer.toJson(invidiousInstance), + 'themeMode': serializer.toJson(themeMode), + 'audioSource': serializer.toJson(audioSource), + 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), + 'streamMusicCodec': serializer.toJson(streamMusicCodec), + 'downloadMusicCodec': serializer.toJson(downloadMusicCodec), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + 'connectPort': serializer.toJson(connectPort), + 'cacheMusic': serializer.toJson(cacheMusic), + }; + } + + PreferencesTableData copyWith( + {int? id, + String? audioQuality, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + String? closeBehavior, + String? accentColorScheme, + String? layoutMode, + String? locale, + String? market, + String? searchMode, + String? downloadLocation, + String? localLibraryLocation, + String? pipedInstance, + String? invidiousInstance, + String? themeMode, + String? audioSource, + String? youtubeClientEngine, + String? streamMusicCodec, + String? downloadMusicCodec, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect, + int? connectPort, + bool? cacheMusic}) => + PreferencesTableData( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { + return PreferencesTableData( + id: data.id.present ? data.id.value : this.id, + audioQuality: data.audioQuality.present + ? data.audioQuality.value + : this.audioQuality, + albumColorSync: data.albumColorSync.present + ? data.albumColorSync.value + : this.albumColorSync, + amoledDarkTheme: data.amoledDarkTheme.present + ? data.amoledDarkTheme.value + : this.amoledDarkTheme, + checkUpdate: + data.checkUpdate.present ? data.checkUpdate.value : this.checkUpdate, + normalizeAudio: data.normalizeAudio.present + ? data.normalizeAudio.value + : this.normalizeAudio, + showSystemTrayIcon: data.showSystemTrayIcon.present + ? data.showSystemTrayIcon.value + : this.showSystemTrayIcon, + systemTitleBar: data.systemTitleBar.present + ? data.systemTitleBar.value + : this.systemTitleBar, + skipNonMusic: data.skipNonMusic.present + ? data.skipNonMusic.value + : this.skipNonMusic, + closeBehavior: data.closeBehavior.present + ? data.closeBehavior.value + : this.closeBehavior, + accentColorScheme: data.accentColorScheme.present + ? data.accentColorScheme.value + : this.accentColorScheme, + layoutMode: + data.layoutMode.present ? data.layoutMode.value : this.layoutMode, + locale: data.locale.present ? data.locale.value : this.locale, + market: data.market.present ? data.market.value : this.market, + searchMode: + data.searchMode.present ? data.searchMode.value : this.searchMode, + downloadLocation: data.downloadLocation.present + ? data.downloadLocation.value + : this.downloadLocation, + localLibraryLocation: data.localLibraryLocation.present + ? data.localLibraryLocation.value + : this.localLibraryLocation, + pipedInstance: data.pipedInstance.present + ? data.pipedInstance.value + : this.pipedInstance, + invidiousInstance: data.invidiousInstance.present + ? data.invidiousInstance.value + : this.invidiousInstance, + themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, + audioSource: + data.audioSource.present ? data.audioSource.value : this.audioSource, + youtubeClientEngine: data.youtubeClientEngine.present + ? data.youtubeClientEngine.value + : this.youtubeClientEngine, + streamMusicCodec: data.streamMusicCodec.present + ? data.streamMusicCodec.value + : this.streamMusicCodec, + downloadMusicCodec: data.downloadMusicCodec.present + ? data.downloadMusicCodec.value + : this.downloadMusicCodec, + discordPresence: data.discordPresence.present + ? data.discordPresence.value + : this.discordPresence, + endlessPlayback: data.endlessPlayback.present + ? data.endlessPlayback.value + : this.endlessPlayback, + enableConnect: data.enableConnect.present + ? data.enableConnect.value + : this.enableConnect, + connectPort: + data.connectPort.present ? data.connectPort.value : this.connectPort, + cacheMusic: + data.cacheMusic.present ? data.cacheMusic.value : this.cacheMusic, + ); + } + + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSource, + youtubeClientEngine, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.audioQuality == this.audioQuality && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.invidiousInstance == this.invidiousInstance && + other.themeMode == this.themeMode && + other.audioSource == this.audioSource && + other.youtubeClientEngine == this.youtubeClientEngine && + other.streamMusicCodec == this.streamMusicCodec && + other.downloadMusicCodec == this.downloadMusicCodec && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect && + other.connectPort == this.connectPort && + other.cacheMusic == this.cacheMusic); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value audioQuality; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value localLibraryLocation; + final Value pipedInstance; + final Value invidiousInstance; + final Value themeMode; + final Value audioSource; + final Value youtubeClientEngine; + final Value streamMusicCodec; + final Value downloadMusicCodec; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + final Value connectPort; + final Value cacheMusic; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? audioQuality, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? invidiousInstance, + Expression? themeMode, + Expression? audioSource, + Expression? youtubeClientEngine, + Expression? streamMusicCodec, + Expression? downloadMusicCodec, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + Expression? connectPort, + Expression? cacheMusic, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioQuality != null) 'audio_quality': audioQuality, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (invidiousInstance != null) 'invidious_instance': invidiousInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSource != null) 'audio_source': audioSource, + if (youtubeClientEngine != null) + 'youtube_client_engine': youtubeClientEngine, + if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, + if (downloadMusicCodec != null) + 'download_music_codec': downloadMusicCodec, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + if (connectPort != null) 'connect_port': connectPort, + if (cacheMusic != null) 'cache_music': cacheMusic, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? audioQuality, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value? localLibraryLocation, + Value? pipedInstance, + Value? invidiousInstance, + Value? themeMode, + Value? audioSource, + Value? youtubeClientEngine, + Value? streamMusicCodec, + Value? downloadMusicCodec, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect, + Value? connectPort, + Value? cacheMusic}) { + return PreferencesTableCompanion( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioQuality.present) { + map['audio_quality'] = Variable(audioQuality.value); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable(closeBehavior.value); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable(accentColorScheme.value); + } + if (layoutMode.present) { + map['layout_mode'] = Variable(layoutMode.value); + } + if (locale.present) { + map['locale'] = Variable(locale.value); + } + if (market.present) { + map['market'] = Variable(market.value); + } + if (searchMode.present) { + map['search_mode'] = Variable(searchMode.value); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = + Variable(localLibraryLocation.value); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (invidiousInstance.present) { + map['invidious_instance'] = Variable(invidiousInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable(themeMode.value); + } + if (audioSource.present) { + map['audio_source'] = Variable(audioSource.value); + } + if (youtubeClientEngine.present) { + map['youtube_client_engine'] = + Variable(youtubeClientEngine.value); + } + if (streamMusicCodec.present) { + map['stream_music_codec'] = Variable(streamMusicCodec.value); + } + if (downloadMusicCodec.present) { + map['download_music_codec'] = Variable(downloadMusicCodec.value); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + if (connectPort.present) { + map['connect_port'] = Variable(connectPort.value); + } + if (cacheMusic.present) { + map['cache_music'] = Variable(cacheMusic.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } +} + +class ScrobblerTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ScrobblerTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + ScrobblerTable createAlias(String alias) { + return ScrobblerTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + ScrobblerTableData copyWithCompanion(ScrobblerTableCompanion data) { + return ScrobblerTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + username: data.username.present ? data.username.value : this.username, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + ); + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + +class SkipSegmentTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SkipSegmentTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_table'; + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SkipSegmentTable createAlias(String alias) { + return SkipSegmentTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + SkipSegmentTableData copyWithCompanion(SkipSegmentTableCompanion data) { + return SkipSegmentTableData( + id: data.id.present ? data.id.value : this.id, + start: data.start.present ? data.start.value : this.start, + end: data.end.present ? data.end.value : this.end, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SourceMatchTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SourceMatchTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceType.youtube.name)); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceId, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_table'; + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SourceMatchTable createAlias(String alias) { + return SourceMatchTable(attachedDatabase, alias); + } +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceId; + final String sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceId, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_id'] = Variable(sourceId); + map['source_type'] = Variable(sourceType); + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceId: Value(sourceId), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceId: serializer.fromJson(json['sourceId']), + sourceType: serializer.fromJson(json['sourceType']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceId': serializer.toJson(sourceId), + 'sourceType': serializer.toJson(sourceType), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceId, + String? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + SourceMatchTableData copyWithCompanion(SourceMatchTableCompanion data) { + return SourceMatchTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + sourceType: + data.sourceType.present ? data.sourceType.value : this.sourceType, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceId == this.sourceId && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceId; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceId = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceId, + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceId = Value(sourceId); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceId, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceId != null) 'source_id': sourceId, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceId, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class AudioPlayerStateTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AudioPlayerStateTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + late final GeneratedColumn loopMode = GeneratedColumn( + 'loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + late final GeneratedColumn collections = GeneratedColumn( + 'collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn tracks = GeneratedColumn( + 'tracks', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("[]")); + late final GeneratedColumn currentIndex = GeneratedColumn( + 'current_index', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + List get $columns => + [id, playing, loopMode, shuffled, collections, tracks, currentIndex]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_table'; + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + loopMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!, + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}collections'])!, + tracks: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}tracks'])!, + currentIndex: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}current_index'])!, + ); + } + + @override + AudioPlayerStateTable createAlias(String alias) { + return AudioPlayerStateTable(attachedDatabase, alias); + } +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final String loopMode; + final bool shuffled; + final String collections; + final String tracks; + final int currentIndex; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.loopMode, + required this.shuffled, + required this.collections, + required this.tracks, + required this.currentIndex}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['loop_mode'] = Variable(loopMode); + map['shuffled'] = Variable(shuffled); + map['collections'] = Variable(collections); + map['tracks'] = Variable(tracks); + map['current_index'] = Variable(currentIndex); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + collections: Value(collections), + tracks: Value(tracks), + currentIndex: Value(currentIndex), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + loopMode: serializer.fromJson(json['loopMode']), + shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson(json['collections']), + tracks: serializer.fromJson(json['tracks']), + currentIndex: serializer.fromJson(json['currentIndex']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'loopMode': serializer.toJson(loopMode), + 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson(collections), + 'tracks': serializer.toJson(tracks), + 'currentIndex': serializer.toJson(currentIndex), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + String? loopMode, + bool? shuffled, + String? collections, + String? tracks, + int? currentIndex}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + AudioPlayerStateTableData copyWithCompanion( + AudioPlayerStateTableCompanion data) { + return AudioPlayerStateTableData( + id: data.id.present ? data.id.value : this.id, + playing: data.playing.present ? data.playing.value : this.playing, + loopMode: data.loopMode.present ? data.loopMode.value : this.loopMode, + shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled, + collections: + data.collections.present ? data.collections.value : this.collections, + tracks: data.tracks.present ? data.tracks.value : this.tracks, + currentIndex: data.currentIndex.present + ? data.currentIndex.value + : this.currentIndex, + ); + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, playing, loopMode, shuffled, collections, tracks, currentIndex); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled && + other.collections == this.collections && + other.tracks == this.tracks && + other.currentIndex == this.currentIndex); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value loopMode; + final Value shuffled; + final Value collections; + final Value tracks; + final Value currentIndex; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + this.collections = const Value.absent(), + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required String loopMode, + required bool shuffled, + required String collections, + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }) : playing = Value(playing), + loopMode = Value(loopMode), + shuffled = Value(shuffled), + collections = Value(collections); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? loopMode, + Expression? shuffled, + Expression? collections, + Expression? tracks, + Expression? currentIndex, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, + if (tracks != null) 'tracks': tracks, + if (currentIndex != null) 'current_index': currentIndex, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? loopMode, + Value? shuffled, + Value? collections, + Value? tracks, + Value? currentIndex}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable(loopMode.value); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + if (collections.present) { + map['collections'] = Variable(collections.value); + } + if (tracks.present) { + map['tracks'] = Variable(tracks.value); + } + if (currentIndex.present) { + map['current_index'] = Variable(currentIndex.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } +} + +class HistoryTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + HistoryTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + HistoryTable createAlias(String alias) { + return HistoryTable(attachedDatabase, alias); + } +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String type; + final String itemId; + final String data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['type'] = Variable(type); + map['item_id'] = Variable(itemId); + map['data'] = Variable(data); + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: serializer.fromJson(json['type']), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer.toJson(type), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + String? type, + String? itemId, + String? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + HistoryTableData copyWithCompanion(HistoryTableCompanion data) { + return HistoryTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + type: data.type.present ? data.type.value : this.type, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String type, + required String itemId, + required String data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class LyricsTable extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LyricsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, trackId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lyrics_table'; + @override + Set get $primaryKey => {id}; + @override + LyricsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LyricsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + LyricsTable createAlias(String alias) { + return LyricsTable(attachedDatabase, alias); + } +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final String data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['data'] = Variable(data); + return map; + } + + LyricsTableCompanion toCompanion(bool nullToAbsent) { + return LyricsTableCompanion( + id: Value(id), + trackId: Value(trackId), + data: Value(data), + ); + } + + factory LyricsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(data), + }; + } + + LyricsTableData copyWith({int? id, String? trackId, String? data}) => + LyricsTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + LyricsTableData copyWithCompanion(LyricsTableCompanion data) { + return LyricsTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('LyricsTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LyricsTableData && + other.id == this.id && + other.trackId == this.trackId && + other.data == this.data); +} + +class LyricsTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value data; + const LyricsTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.data = const Value.absent(), + }); + LyricsTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String data, + }) : trackId = Value(trackId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LyricsTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class PluginsTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PluginsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn author = GeneratedColumn( + 'author', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entryPoint = GeneratedColumn( + 'entry_point', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apis = GeneratedColumn( + 'apis', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn abilities = GeneratedColumn( + 'abilities', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn repository = GeneratedColumn( + 'repository', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn pluginApiVersion = GeneratedColumn( + 'plugin_api_version', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('2.0.0')); + @override + List get $columns => [ + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'plugins_table'; + @override + Set get $primaryKey => {id}; + @override + PluginsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PluginsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + version: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}version'])!, + author: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}author'])!, + entryPoint: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, + apis: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}apis'])!, + abilities: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!, + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, + repository: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}repository']), + pluginApiVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}plugin_api_version'])!, + ); + } + + @override + PluginsTable createAlias(String alias) { + return PluginsTable(attachedDatabase, alias); + } +} + +class PluginsTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String description; + final String version; + final String author; + final String entryPoint; + final String apis; + final String abilities; + final bool selectedForMetadata; + final bool selectedForAudioSource; + final String? repository; + final String pluginApiVersion; + const PluginsTableData( + {required this.id, + required this.name, + required this.description, + required this.version, + required this.author, + required this.entryPoint, + required this.apis, + required this.abilities, + required this.selectedForMetadata, + required this.selectedForAudioSource, + this.repository, + required this.pluginApiVersion}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['version'] = Variable(version); + map['author'] = Variable(author); + map['entry_point'] = Variable(entryPoint); + map['apis'] = Variable(apis); + map['abilities'] = Variable(abilities); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); + if (!nullToAbsent || repository != null) { + map['repository'] = Variable(repository); + } + map['plugin_api_version'] = Variable(pluginApiVersion); + return map; + } + + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( + id: Value(id), + name: Value(name), + description: Value(description), + version: Value(version), + author: Value(author), + entryPoint: Value(entryPoint), + apis: Value(apis), + abilities: Value(abilities), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), + repository: repository == null && nullToAbsent + ? const Value.absent() + : Value(repository), + pluginApiVersion: Value(pluginApiVersion), + ); + } + + factory PluginsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PluginsTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + version: serializer.fromJson(json['version']), + author: serializer.fromJson(json['author']), + entryPoint: serializer.fromJson(json['entryPoint']), + apis: serializer.fromJson(json['apis']), + abilities: serializer.fromJson(json['abilities']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), + repository: serializer.fromJson(json['repository']), + pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'version': serializer.toJson(version), + 'author': serializer.toJson(author), + 'entryPoint': serializer.toJson(entryPoint), + 'apis': serializer.toJson(apis), + 'abilities': serializer.toJson(abilities), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), + 'repository': serializer.toJson(repository), + 'pluginApiVersion': serializer.toJson(pluginApiVersion), + }; + } + + PluginsTableData copyWith( + {int? id, + String? name, + String? description, + String? version, + String? author, + String? entryPoint, + String? apis, + String? abilities, + bool? selectedForMetadata, + bool? selectedForAudioSource, + Value repository = const Value.absent(), + String? pluginApiVersion}) => + PluginsTableData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository.present ? repository.value : this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + version: data.version.present ? data.version.value : this.version, + author: data.author.present ? data.author.value : this.author, + entryPoint: + data.entryPoint.present ? data.entryPoint.value : this.entryPoint, + apis: data.apis.present ? data.apis.value : this.apis, + abilities: data.abilities.present ? data.abilities.value : this.abilities, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, + repository: + data.repository.present ? data.repository.value : this.repository, + pluginApiVersion: data.pluginApiVersion.present + ? data.pluginApiVersion.value + : this.pluginApiVersion, + ); + } + + @override + String toString() { + return (StringBuffer('PluginsTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PluginsTableData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.version == this.version && + other.author == this.author && + other.entryPoint == this.entryPoint && + other.apis == this.apis && + other.abilities == this.abilities && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && + other.repository == this.repository && + other.pluginApiVersion == this.pluginApiVersion); +} + +class PluginsTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value version; + final Value author; + final Value entryPoint; + final Value apis; + final Value abilities; + final Value selectedForMetadata; + final Value selectedForAudioSource; + final Value repository; + final Value pluginApiVersion; + const PluginsTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.version = const Value.absent(), + this.author = const Value.absent(), + this.entryPoint = const Value.absent(), + this.apis = const Value.absent(), + this.abilities = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }); + PluginsTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String description, + required String version, + required String author, + required String entryPoint, + required String apis, + required String abilities, + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }) : name = Value(name), + description = Value(description), + version = Value(version), + author = Value(author), + entryPoint = Value(entryPoint), + apis = Value(apis), + abilities = Value(abilities); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? version, + Expression? author, + Expression? entryPoint, + Expression? apis, + Expression? abilities, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, + Expression? repository, + Expression? pluginApiVersion, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (version != null) 'version': version, + if (author != null) 'author': author, + if (entryPoint != null) 'entry_point': entryPoint, + if (apis != null) 'apis': apis, + if (abilities != null) 'abilities': abilities, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, + if (repository != null) 'repository': repository, + if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, + }); + } + + PluginsTableCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? version, + Value? author, + Value? entryPoint, + Value? apis, + Value? abilities, + Value? selectedForMetadata, + Value? selectedForAudioSource, + Value? repository, + Value? pluginApiVersion}) { + return PluginsTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository ?? this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (version.present) { + map['version'] = Variable(version.value); + } + if (author.present) { + map['author'] = Variable(author.value); + } + if (entryPoint.present) { + map['entry_point'] = Variable(entryPoint.value); + } + if (apis.present) { + map['apis'] = Variable(apis.value); + } + if (abilities.present) { + map['abilities'] = Variable(abilities.value); + } + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); + } + if (repository.present) { + map['repository'] = Variable(repository.value); + } + if (pluginApiVersion.present) { + map['plugin_api_version'] = Variable(pluginApiVersion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PluginsTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV9 extends GeneratedDatabase { + DatabaseAtV9(QueryExecutor e) : super(e); + late final AuthenticationTable authenticationTable = + AuthenticationTable(this); + late final BlacklistTable blacklistTable = BlacklistTable(this); + late final PreferencesTable preferencesTable = PreferencesTable(this); + late final ScrobblerTable scrobblerTable = ScrobblerTable(this); + late final SkipSegmentTable skipSegmentTable = SkipSegmentTable(this); + late final SourceMatchTable sourceMatchTable = SourceMatchTable(this); + late final AudioPlayerStateTable audioPlayerStateTable = + AudioPlayerStateTable(this); + late final HistoryTable historyTable = HistoryTable(this); + late final LyricsTable lyricsTable = LyricsTable(this); + late final PluginsTable pluginsTable = PluginsTable(this); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch + ]; + @override + int get schemaVersion => 9; +} diff --git a/untranslated_messages.json b/untranslated_messages.json index 618ddcf8..bcde408d 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,5 +1,9 @@ { "ar": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -8,6 +12,10 @@ ], "bn": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -16,6 +24,10 @@ ], "ca": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -24,6 +36,10 @@ ], "cs": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -32,6 +48,10 @@ ], "de": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -40,6 +60,10 @@ ], "es": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -48,6 +72,10 @@ ], "eu": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -56,6 +84,10 @@ ], "fa": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -64,6 +96,10 @@ ], "fi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -72,6 +108,10 @@ ], "fr": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -80,6 +120,10 @@ ], "hi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -88,6 +132,10 @@ ], "id": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -96,6 +144,10 @@ ], "it": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -104,6 +156,10 @@ ], "ja": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -112,6 +168,10 @@ ], "ka": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -120,6 +180,10 @@ ], "ko": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -128,6 +192,10 @@ ], "ne": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -137,6 +205,10 @@ "nl": [ "audio_source", + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -145,6 +217,10 @@ ], "pl": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -153,6 +229,10 @@ ], "pt": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -161,6 +241,10 @@ ], "ru": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -169,6 +253,10 @@ ], "ta": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -177,6 +265,10 @@ ], "th": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -185,6 +277,10 @@ ], "tl": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -193,6 +289,10 @@ ], "tr": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -201,6 +301,10 @@ ], "uk": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -209,6 +313,10 @@ ], "vi": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -217,6 +325,10 @@ ], "zh": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", @@ -225,6 +337,10 @@ ], "zh_TW": [ + "default_metadata_source", + "set_default_metadata_source", + "default_audio_source", + "set_default_audio_source", "plugins", "configure_plugins", "source", From 99a84aa6dcf463a1b814e50a0ddfabb3b8ba0a16 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 13:32:48 +0600 Subject: [PATCH 18/47] chore: create sourced track from active audio source plugin --- lib/models/database/database.dart | 3 +- lib/models/database/tables/preferences.dart | 25 +- lib/models/database/tables/source_match.dart | 18 +- lib/models/playback/track_sources.dart | 116 +- .../local_folder/cache_export_dialog.dart | 1 - lib/modules/player/player.dart | 2 +- lib/modules/root/sidebar/sidebar.dart | 2 +- lib/pages/settings/sections/playback.dart | 1 - lib/provider/download_manager_provider.dart | 1 - lib/provider/server/routes/playback.dart | 1 - .../user_preferences_provider.dart | 1 - lib/services/sourced_track/enums.dart | 34 - .../sourced_track/models/video_info.dart | 136 - lib/services/sourced_track/sourced_track.dart | 470 ++- .../sourced_track/sources/dab_music.dart | 303 -- .../sourced_track/sources/invidious.dart | 263 -- .../sourced_track/sources/jiosaavn.dart | 231 -- lib/services/sourced_track/sources/piped.dart | 292 -- .../sourced_track/sources/youtube.dart | 439 --- .../youtube_explode_engine.dart | 1 - pubspec.lock | 11 +- pubspec.yaml | 3 +- test/drift/app_db/generated/schema.dart | 5 +- test/drift/app_db/generated/schema_v10.dart | 3472 +++++++++++++++++ 24 files changed, 3810 insertions(+), 2021 deletions(-) delete mode 100644 lib/services/sourced_track/enums.dart delete mode 100644 lib/services/sourced_track/models/video_info.dart delete mode 100644 lib/services/sourced_track/sources/dab_music.dart delete mode 100644 lib/services/sourced_track/sources/invidious.dart delete mode 100644 lib/services/sourced_track/sources/jiosaavn.dart delete mode 100644 lib/services/sourced_track/sources/piped.dart delete mode 100644 lib/services/sourced_track/sources/youtube.dart create mode 100644 test/drift/app_db/generated/schema_v10.dart diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 2df41e9a..a03cdb8c 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -16,7 +16,6 @@ import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; @@ -65,7 +64,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 9; + int get schemaVersion => 10; @override MigrationStrategy get migration { diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 64580330..ea2f7538 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -11,17 +11,6 @@ enum CloseBehavior { close, } -enum AudioSource { - youtube("YouTube"), - piped("Piped"), - jiosaavn("JioSaavn"), - invidious("Invidious"), - dabMusic("DAB Music"); - - final String label; - const AudioSource(this.label); -} - enum YoutubeClientEngine { ytDlp("yt-dlp"), youtubeExplode("YouTubeExplode"), @@ -56,8 +45,6 @@ enum SearchMode { class PreferencesTable extends Table { IntColumn get id => integer().autoIncrement()(); - TextColumn get audioQuality => textEnum() - .withDefault(Constant(SourceQualities.high.name))(); BoolColumn get albumColorSync => boolean().withDefault(const Constant(true))(); BoolColumn get amoledDarkTheme => @@ -95,14 +82,9 @@ class PreferencesTable extends Table { text().withDefault(const Constant("https://inv.nadeko.net"))(); TextColumn get themeMode => textEnum().withDefault(Constant(ThemeMode.system.name))(); - TextColumn get audioSource => - textEnum().withDefault(Constant(AudioSource.youtube.name))(); + TextColumn get audioSourceId => text().nullable()(); TextColumn get youtubeClientEngine => textEnum() .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); - TextColumn get streamMusicCodec => - textEnum().withDefault(Constant(SourceCodecs.weba.name))(); - TextColumn get downloadMusicCodec => - textEnum().withDefault(Constant(SourceCodecs.m4a.name))(); BoolColumn get discordPresence => boolean().withDefault(const Constant(true))(); BoolColumn get endlessPlayback => @@ -116,7 +98,6 @@ class PreferencesTable extends Table { static PreferencesTableData defaults() { return PreferencesTableData( id: 0, - audioQuality: SourceQualities.high, albumColorSync: true, amoledDarkTheme: false, checkUpdate: true, @@ -135,10 +116,8 @@ class PreferencesTable extends Table { pipedInstance: "https://pipedapi.kavin.rocks", invidiousInstance: "https://inv.nadeko.net", themeMode: ThemeMode.system, - audioSource: AudioSource.youtube, + audioSourceId: null, youtubeClientEngine: YoutubeClientEngine.youtubeExplode, - streamMusicCodec: SourceCodecs.m4a, - downloadMusicCodec: SourceCodecs.m4a, discordPresence: true, endlessPlayback: true, enableConnect: false, diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index fa659287..b5661137 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -1,26 +1,14 @@ part of '../database.dart'; -enum SourceType { - youtube._("YouTube"), - youtubeMusic._("YouTube Music"), - jiosaavn._("JioSaavn"), - dabMusic._("DAB Music"); - - final String label; - - const SourceType._(this.label); -} - @TableIndex( name: "uniq_track_match", - columns: {#trackId, #sourceId, #sourceType}, + columns: {#trackId, #sourceInfo, #sourceType}, unique: true, ) class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); - TextColumn get sourceId => text()(); - TextColumn get sourceType => - textEnum().withDefault(Constant(SourceType.youtube.name))(); + TextColumn get sourceInfo => text()(); + TextColumn get sourceType => text()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index 1666609c..262fcefa 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -1,122 +1,16 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; part 'track_sources.freezed.dart'; part 'track_sources.g.dart'; -@freezed -class TrackSourceQuery with _$TrackSourceQuery { - TrackSourceQuery._(); - - factory TrackSourceQuery({ - required String id, - required String title, - required List artists, - required String album, - required int durationMs, - required String isrc, - required bool explicit, - }) = _TrackSourceQuery; - - factory TrackSourceQuery.fromJson(Map json) => - _$TrackSourceQueryFromJson(json); - - factory TrackSourceQuery.fromTrack(SpotubeFullTrackObject track) { - return TrackSourceQuery( - id: track.id, - title: track.name, - artists: track.artists.map((e) => e.name).toList(), - album: track.album.name, - durationMs: track.durationMs, - isrc: track.isrc, - explicit: track.explicit, - ); - } - - /// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery]. - factory TrackSourceQuery.parseUri(String url) { - final isLocal = !url.startsWith("http"); - - if (isLocal) { - try { - return TrackSourceQuery( - id: url, - title: '', - artists: [], - album: '', - durationMs: 0, - isrc: '', - explicit: false, - ); - } catch (e, stackTrace) { - AppLogger.log.e( - "Failed to parse local track URI: $url\n$e", - stackTrace: stackTrace, - ); - } - } - - final uri = Uri.parse(url); - return TrackSourceQuery( - id: uri.pathSegments.last, - title: uri.queryParameters['title'] ?? '', - artists: uri.queryParameters['artists']?.split(',') ?? [], - album: uri.queryParameters['album'] ?? '', - durationMs: int.tryParse(uri.queryParameters['durationMs'] ?? '0') ?? 0, - isrc: uri.queryParameters['isrc'] ?? '', - explicit: uri.queryParameters['explicit']?.toLowerCase() == 'true', - ); - } - - String queryString() { - return toJson() - .entries - .map((e) => - "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List ? e.value.join(",") : e.value.toString())}") - .join("&"); - } -} - -@freezed -class TrackSourceInfo with _$TrackSourceInfo { - factory TrackSourceInfo({ - required String id, - required String title, - required String artists, - required String thumbnail, - required String pageUrl, - required int durationMs, - }) = _TrackSourceInfo; - - factory TrackSourceInfo.fromJson(Map json) => - _$TrackSourceInfoFromJson(json); -} - -@freezed -class TrackSource with _$TrackSource { - factory TrackSource({ - required String url, - required SourceQualities quality, - required SourceCodecs codec, - required String bitrate, - required String qualityLabel, - }) = _TrackSource; - - factory TrackSource.fromJson(Map json) => - _$TrackSourceFromJson(json); -} - @JsonSerializable() class BasicSourcedTrack { - final TrackSourceQuery query; - final AudioSource source; - final TrackSourceInfo info; - final List sources; - final List siblings; + final SpotubeFullTrackObject query; + final SpotubeAudioSourceMatchObject info; + final String source; + final List sources; + final List siblings; BasicSourcedTrack({ required this.query, required this.source, diff --git a/lib/modules/library/local_folder/cache_export_dialog.dart b/lib/modules/library/local_folder/cache_export_dialog.dart index 0f10defc..fde219c9 100644 --- a/lib/modules/library/local_folder/cache_export_dialog.dart +++ b/lib/modules/library/local_folder/cache_export_dialog.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as path; import 'package:spotube/extensions/context.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; final codecs = SourceCodecs.values.map((s) => s.name); diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 4250e153..69262641 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -49,7 +49,7 @@ class PlayerView extends HookConsumerWidget { final activeSourceCodec = useMemoized( () { return currentActiveTrackSource - ?.getSourceOfCodec(currentActiveTrackSource.codec); + ?.getStreamOfCodec(currentActiveTrackSource.codec); }, [currentActiveTrackSource?.sources, currentActiveTrackSource?.codec], ); diff --git a/lib/modules/root/sidebar/sidebar.dart b/lib/modules/root/sidebar/sidebar.dart index e4e7db3d..1538d624 100644 --- a/lib/modules/root/sidebar/sidebar.dart +++ b/lib/modules/root/sidebar/sidebar.dart @@ -22,7 +22,7 @@ class Sidebar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ThemeData(:colorScheme) = Theme.of(context); - final mediaQuery = MediaQuery.of(context); + final mediaQuery = MediaQuery.sizeOf(context); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 6d0b5dc3..77eaa0c5 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -23,7 +23,6 @@ import 'package:spotube/provider/audio_player/sources/piped_instances_provider.d import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index d7f28b67..d0112765 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -12,7 +12,6 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 4bce7444..7155edca 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -20,7 +20,6 @@ import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 8e72727c..9bc64f4f 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -10,7 +10,6 @@ import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import 'package:open_file/open_file.dart'; diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart deleted file mode 100644 index 9a1a5040..00000000 --- a/lib/services/sourced_track/enums.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:spotube/models/playback/track_sources.dart'; - -enum SourceCodecs { - m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"), - mp3._("MP3 (Widely supported audio format)"), - flac._("FLAC (Lossless, best quality)\nLarge file size"); - - final String label; - const SourceCodecs._(this.label); -} - -enum SourceQualities { - uncompressed(3), - high(2), - medium(1), - low(0); - - final int priority; - const SourceQualities(this.priority); - - bool operator <(SourceQualities other) { - return priority < other.priority; - } - - operator >(SourceQualities other) { - return priority > other.priority; - } -} - -typedef SiblingType = ({ - T info, - List? source -}); diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart deleted file mode 100644 index e3452c61..00000000 --- a/lib/services/sourced_track/models/video_info.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:invidious/invidious.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/models/database/database.dart'; - -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - -class YoutubeVideoInfo { - final SearchMode searchMode; - final String title; - final Duration duration; - final String thumbnailUrl; - final String id; - final int likes; - final int dislikes; - final int views; - final String channelName; - final String channelId; - final DateTime publishedAt; - - YoutubeVideoInfo({ - required this.searchMode, - required this.title, - required this.duration, - required this.thumbnailUrl, - required this.id, - required this.likes, - required this.dislikes, - required this.views, - required this.channelName, - required this.publishedAt, - required this.channelId, - }); - - YoutubeVideoInfo.fromJson(Map json) - : title = json['title'], - searchMode = SearchMode.fromString(json['searchMode']), - duration = Duration(seconds: json['duration']), - thumbnailUrl = json['thumbnailUrl'], - id = json['id'], - likes = json['likes'], - dislikes = json['dislikes'], - views = json['views'], - channelName = json['channelName'], - channelId = json['channelId'], - publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now(); - - Map toJson() => { - 'title': title, - 'duration': duration.inSeconds, - 'thumbnailUrl': thumbnailUrl, - 'id': id, - 'likes': likes, - 'dislikes': dislikes, - 'views': views, - 'channelName': channelName, - 'channelId': channelId, - 'publishedAt': publishedAt.toIso8601String(), - 'searchMode': searchMode.name, - }; - - factory YoutubeVideoInfo.fromVideo(Video video) { - return YoutubeVideoInfo( - searchMode: SearchMode.youtube, - title: video.title, - duration: video.duration ?? Duration.zero, - thumbnailUrl: video.thumbnails.mediumResUrl, - id: video.id.value, - likes: video.engagement.likeCount ?? 0, - dislikes: video.engagement.dislikeCount ?? 0, - views: video.engagement.viewCount, - channelName: video.author, - channelId: '/c/${video.channelId.value}', - publishedAt: video.uploadDate ?? DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromSearchItemStream( - PipedSearchItemStream searchItem, - SearchMode searchMode, - ) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: searchItem.title, - duration: searchItem.duration, - thumbnailUrl: searchItem.thumbnail, - id: searchItem.id, - likes: 0, - dislikes: 0, - views: searchItem.views, - channelName: searchItem.uploaderName, - channelId: searchItem.uploaderUrl ?? "", - publishedAt: searchItem.uploadedDate != null - ? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromStreamResponse( - PipedStreamResponse stream, SearchMode searchMode) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: stream.title, - duration: stream.duration, - thumbnailUrl: stream.thumbnailUrl, - id: stream.id, - likes: stream.likes, - dislikes: stream.dislikes, - views: stream.views, - channelName: stream.uploader, - publishedAt: stream.uploadedDate != null - ? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - channelId: stream.uploaderUrl, - ); - } - - factory YoutubeVideoInfo.fromSearchResponse( - InvidiousSearchResponseVideo searchResponse, - SearchMode searchMode, - ) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: searchResponse.title, - duration: Duration(seconds: searchResponse.lengthSeconds), - thumbnailUrl: searchResponse.videoThumbnails.first.url, - id: searchResponse.videoId, - likes: 0, - dislikes: 0, - views: searchResponse.viewCount, - channelName: searchResponse.author, - channelId: searchResponse.authorId, - publishedAt: - DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000), - ); - } -} diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a5b2ae93..661a8447 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -1,18 +1,28 @@ -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'dart:convert'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/sources/dab_music.dart'; -import 'package:spotube/services/sourced_track/sources/invidious.dart'; -import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; -import 'package:spotube/services/sourced_track/sources/piped.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/extensions/string.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/dio/dio.dart'; +import 'package:spotube/services/logger/logger.dart'; + +import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/utils/service_utils.dart'; -abstract class SourcedTrack extends BasicSourcedTrack { +final officialMusicRegex = RegExp( + r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", + caseSensitive: false, +); + +class SourcedTrack extends BasicSourcedTrack { final Ref ref; SourcedTrack({ @@ -24,72 +34,10 @@ abstract class SourcedTrack extends BasicSourcedTrack { required super.sources, }); - static SourcedTrack fromJson( - Map json, { - required Ref ref, - }) { - final preferences = ref.read(userPreferencesProvider); - - final info = TrackSourceInfo.fromJson(json["info"]); - final query = TrackSourceQuery.fromJson(json["query"]); - final source = AudioSource.values.firstWhereOrNull( - (source) => source.name == json["source"], - ) ?? - preferences.audioSource; - final siblings = (json["siblings"] as List) - .map((s) => TrackSourceInfo.fromJson(s)) - .toList(); - final sources = - (json["sources"] as List).map((s) => TrackSource.fromJson(s)).toList(); - - return switch (preferences.audioSource) { - AudioSource.youtube => YoutubeSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.piped => PipedSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.jiosaavn => JioSaavnSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.invidious => InvidiousSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - AudioSource.dabMusic => DABMusicSourcedTrack( - ref: ref, - source: source, - siblings: siblings, - info: info, - query: query, - sources: sources, - ), - }; - } - - static String getSearchTerm(TrackSourceQuery track) { + static String getSearchTerm(SpotubeFullTrackObject track) { final title = ServiceUtils.getTitle( - track.title, - artists: track.artists, + track.name, + artists: track.artists.map((e) => e.name).toList(), onlyCleanArtist: true, ).trim(); @@ -99,61 +47,256 @@ abstract class SourcedTrack extends BasicSourcedTrack { } static Future fetchFromQuery({ - required TrackSourceQuery query, + required SpotubeFullTrackObject query, required Ref ref, }) async { - final preferences = ref.read(userPreferencesProvider); - try { - return switch (preferences.audioSource) { - AudioSource.youtube => - await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.piped => - await PipedSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.invidious => - await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.jiosaavn => - await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref), - AudioSource.dabMusic => - await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref), - }; - } catch (e) { - if (preferences.audioSource == AudioSource.youtube) { - rethrow; + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => + s.trackId.equals(query.id) & + s.sourceType.equals(audioSourceConfig.slug)) + ..limit(1) + ..orderBy([ + (s) => + OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), + ])) + .get() + .then((s) => s.firstOrNull); + + if (cachedSource == null) { + final siblings = await fetchSiblings(ref: ref, query: query); + if (siblings.isEmpty) { + throw TrackNotFoundError(query); } - return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + source: jsonEncode(siblings.first), + sourceType: Value(audioSourceConfig.slug), + ), + ); + + return SourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + info: siblings.first.info, + source: audioSourceConfig.slug, + sources: siblings.first.source ?? [], + query: query, + ); } + final item = + SpotubeAudioSourceMatchObject.fromJson(jsonDecode(cachedSource.source)); + final manifest = await audioSource.audioSource.streams(item); + + final sourcedTrack = SourcedTrack( + ref: ref, + siblings: [], + sources: manifest, + info: item, + query: query, + source: audioSourceConfig.slug, + ); + + AppLogger.log.i("${query.name}: ${sourcedTrack.url}"); + + return sourcedTrack; } - static Future> fetchSiblings({ - required TrackSourceQuery query, + static List rankResults( + List results, + SpotubeFullTrackObject track, + ) { + return results + .map((sibling) { + int score = 0; + + for (final artist in track.artists) { + final isSameChannelArtist = + sibling.artists.any((a) => a.toLowerCase() == artist.name); + + if (isSameChannelArtist) { + score += 1; + } + + final titleContainsArtist = + sibling.title.toLowerCase().contains(artist.name.toLowerCase()); + + if (titleContainsArtist) { + score += 1; + } + } + + final titleContainsTrackName = + sibling.title.toLowerCase().contains(track.name.toLowerCase()); + + final hasOfficialFlag = + officialMusicRegex.hasMatch(sibling.title.toLowerCase()); + + if (titleContainsTrackName) { + score += 3; + } + + if (hasOfficialFlag) { + score += 1; + } + + if (hasOfficialFlag && titleContainsTrackName) { + score += 2; + } + + return (sibling: sibling, score: score); + }) + .sorted((a, b) => b.score.compareTo(a.score)) + .map((e) => e.sibling) + .toList(); + } + + static Future> fetchSiblings({ + required SpotubeFullTrackObject query, required Ref ref, - }) { - final preferences = ref.read(userPreferencesProvider); + }) async { + final audioSource = await ref.read(audioSourcePluginProvider.future); - return switch (preferences.audioSource) { - AudioSource.piped => - PipedSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.youtube => - YoutubeSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.jiosaavn => - JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.invidious => - InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref), - AudioSource.dabMusic => - DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref), - }; + if (audioSource == null) { + throw Exception("Dude wat?"); + } + + final videoResults = []; + + final searchResults = await audioSource.audioSource.matches(query); + + if (ServiceUtils.onlyContainsEnglish(query.name)) { + videoResults.addAll(searchResults); + } else { + videoResults.addAll(rankResults(searchResults, query)); + } + + return videoResults.toSet().toList(); } - Future copyWithSibling(); + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - Future swapWithSibling(TrackSourceInfo sibling); + return SourcedTrack( + ref: ref, + siblings: fetchedSiblings.where((s) => s.id != info.id).toList(), + source: source, + sources: sources, + info: info, + query: query, + ); + } + + Future swapWithSibling( + SpotubeAudioSourceMatchObject sibling) async { + if (sibling.id == info.id) { + return null; + } + + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); + + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, info); + + final manifest = await audioSource.audioSource.streams(newSourceInfo); + + final database = ref.read(databaseProvider); + + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: query.id, + source: jsonEncode(siblings.first), + sourceType: Value(audioSourceConfig.slug), + createdAt: Value(DateTime.now()), + ), + mode: InsertMode.replace, + ); + + return SourcedTrack( + ref: ref, + source: source, + siblings: newSiblings, + sources: manifest, + info: newSourceInfo, + query: query, + ); + } Future swapWithSiblingOfIndex(int index) { return swapWithSibling(siblings[index]); } - Future refreshStream(); + Future refreshStream() async { + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + List validStreams = []; + + final stringBuffer = StringBuffer(); + for (final source in sources) { + final res = await globalDio.head( + source.url, + options: + Options(validateStatus: (status) => status != null && status < 500), + ); + + stringBuffer.writeln( + "[${query.id}] ${res.statusCode} ${source.container} ${source.codec} ${source.bitrate}", + ); + + if (res.statusCode! < 400) { + validStreams.add(source); + } + } + + AppLogger.log.d(stringBuffer.toString()); + + if (validStreams.isEmpty) { + validStreams = await audioSource.audioSource.streams(info); + } + + final sourcedTrack = SourcedTrack( + ref: ref, + siblings: siblings, + source: source, + sources: validStreams, + info: info, + query: query, + ); + + AppLogger.log.i("Refreshing ${query.name}: ${sourcedTrack.url}"); + + return sourcedTrack; + } + String? get url { final preferences = ref.read(userPreferencesProvider); @@ -170,58 +313,75 @@ abstract class SourcedTrack extends BasicSourcedTrack { /// /// If no sources match the codec, it will return the first or last source /// based on the user's audio quality preference. - TrackSource? getSourceOfCodec(SourceCodecs codec) { - final preferences = ref.read(userPreferencesProvider); + SpotubeAudioSourceStreamObject? getStreamOfQuality( + SpotubeAudioSourceContainerPreset preset, + int qualityIndex, + ) { + final quality = preset.qualities[qualityIndex]; final exactMatch = sources.firstWhereOrNull( - (source) => - source.codec == codec && source.quality == preferences.audioQuality, + (source) { + if (source.container != preset.name) return false; + + if (quality case SpotubeAudioLosslessContainerQuality()) { + return source.sampleRate == quality.sampleRate && + source.bitDepth == quality.bitDepth; + } else { + return source.bitrate == + (preset as SpotubeAudioLossyContainerQuality).bitrate; + } + }, ); if (exactMatch != null) { return exactMatch; } - final sameCodecSources = sources - .where((source) => source.codec == codec) - .toList() - .sorted((a, b) { - final aDiff = (a.quality.index - preferences.audioQuality.index).abs(); - final bDiff = (b.quality.index - preferences.audioQuality.index).abs(); - return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index; - }).toList(); + // Find the closest to preset + SpotubeAudioSourceStreamObject? closest; + for (final source in sources) { + if (source.container != preset.name) continue; - if (sameCodecSources.isNotEmpty) { - return preferences.audioQuality > SourceQualities.low - ? sameCodecSources.first - : sameCodecSources.last; + if (quality case SpotubeAudioLosslessContainerQuality()) { + final sourceBps = (source.bitDepth ?? 0) * (source.sampleRate ?? 0); + final qualityBps = quality.bitDepth * quality.sampleRate; + final closestBps = + (closest?.bitDepth ?? 0) * (closest?.sampleRate ?? 0); + + if (sourceBps == qualityBps) { + closest = source; + break; + } + final closestDiff = (closestBps - qualityBps).abs(); + final sourceDiff = (sourceBps - qualityBps).abs(); + + if (sourceDiff < closestDiff) { + closest = source; + } + } else { + final presetBitrate = + (preset as SpotubeAudioLossyContainerQuality).bitrate; + if (presetBitrate == source.bitrate) { + closest = source; + break; + } + + final closestDiff = (closest?.bitrate ?? 0) - presetBitrate; + final sourceDiff = (source.bitrate ?? 0) - presetBitrate; + + if (sourceDiff < closestDiff) { + closest = source; + } + } } - final fallbackSource = sources.sorted((a, b) { - final aDiff = (a.quality.index - preferences.audioQuality.index).abs(); - final bDiff = (b.quality.index - preferences.audioQuality.index).abs(); - return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index; - }); - - return preferences.audioQuality > SourceQualities.low - ? fallbackSource.firstOrNull - : fallbackSource.lastOrNull; + return closest; } - String? getUrlOfCodec(SourceCodecs codec) { - return getSourceOfCodec(codec)?.url; - } - - SourceCodecs get codec { - final preferences = ref.read(userPreferencesProvider); - - return switch (preferences.audioSource) { - AudioSource.dabMusic => - preferences.audioQuality == SourceQualities.uncompressed - ? SourceCodecs.flac - : SourceCodecs.mp3, - AudioSource.jiosaavn => SourceCodecs.m4a, - _ => preferences.streamMusicCodec - }; + String? getUrlOfQuality( + SpotubeAudioSourceContainerPreset preset, + int qualityIndex, + ) { + return getStreamOfQuality(preset, qualityIndex)?.url; } } diff --git a/lib/services/sourced_track/sources/dab_music.dart b/lib/services/sourced_track/sources/dab_music.dart deleted file mode 100644 index 83cc55b4..00000000 --- a/lib/services/sourced_track/sources/dab_music.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:dab_music_api/dab_music_api.dart'; - -final dabMusicApiClient = DabMusicApiClient( - Dio(), - baseUrl: "https://dab.yeet.su/api", -); - -/// Only Music source that can't support database caching due to having no endpoint. -/// But ISRC search is 100% reliable so caching is actually not necessary. -class DABMusicSourcedTrack extends SourcedTrack { - DABMusicSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - try { - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => OrderingTerm( - expression: s.createdAt, - mode: OrderingMode.desc, - ), - ])) - .get() - .then((s) => s.firstOrNull); - - if (cachedSource != null && - cachedSource.sourceType == SourceType.dabMusic) { - final json = jsonDecode(cachedSource.sourceId); - final info = TrackSourceInfo.fromJson(json["info"]); - final source = (json["sources"] as List?) - ?.map((s) => TrackSource.fromJson(s)) - .toList(); - - final [updatedSource] = await fetchSources( - info.id, - ref.read(userPreferencesProvider).audioQuality, - const AudioQuality( - isHiRes: true, - maximumBitDepth: 16, - maximumSamplingRate: 44.1, - ), - ); - - return DABMusicSourcedTrack( - ref: ref, - source: AudioSource.dabMusic, - siblings: [], - info: info, - query: query, - sources: [ - source!.first.copyWith(url: updatedSource.url), - ], - ); - } - - final siblings = await fetchSiblings(ref: ref, query: query); - - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: jsonEncode({ - "info": siblings.first.info.toJson(), - "sources": (siblings.first.source ?? []) - .map((s) => s.toJson()) - .toList(), - }), - sourceType: const Value(SourceType.dabMusic), - ), - ); - - return DABMusicSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source!, - info: siblings.first.info, - query: query, - source: AudioSource.dabMusic, - ); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future> fetchSources( - String id, - SourceQualities quality, - AudioQuality trackMaximumQuality, - ) async { - try { - final isUncompressed = quality == SourceQualities.uncompressed; - final streamResponse = await dabMusicApiClient.music.getStream( - trackId: id, - quality: isUncompressed ? "27" : "5", - ); - if (streamResponse.url == null) { - throw Exception("No stream URL found for track ID: $id"); - } - - // kbps = (bitDepth * sampleRate * channels) / 1000 - final uncompressedBitrate = !isUncompressed - ? 0 - : ((trackMaximumQuality.maximumBitDepth ?? 0) * - ((trackMaximumQuality.maximumSamplingRate ?? 0) * 1000) * - 2) / - 1000; - return [ - TrackSource( - url: streamResponse.url!, - quality: isUncompressed - ? SourceQualities.uncompressed - : SourceQualities.high, - bitrate: - isUncompressed ? "${uncompressedBitrate.floor()}kbps" : "320kbps", - codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3, - qualityLabel: isUncompressed - ? "${trackMaximumQuality.maximumBitDepth}bit • ${trackMaximumQuality.maximumSamplingRate}kHz • FLAC • Stereo" - : "MP3 • 320kbps • mp3 • Stereo", - ), - ]; - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future toSiblingType( - Ref ref, - int index, - Track result, - ) async { - try { - List? source; - if (index == 0) { - source = await fetchSources( - result.id.toString(), - ref.read(userPreferencesProvider).audioQuality, - result.audioQuality!, - ); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - artists: result.artist!, - durationMs: Duration(seconds: result.duration!).inMilliseconds, - id: result.id.toString(), - pageUrl: "https://dab.yeet.su/music/${result.id}", - thumbnail: result.albumCover!, - title: result.title!, - ), - source: source, - ); - - return sibling; - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - try { - List results = []; - - if (query.isrc.isNotEmpty) { - final res = - await dabMusicApiClient.music.getSearch(q: query.isrc, limit: 1); - results = res.tracks ?? []; - } - - if (results.isEmpty) { - final res = await dabMusicApiClient.music.getSearch( - q: SourcedTrack.getSearchTerm(query), - limit: 5, - ); - results = res.tracks ?? []; - } - - if (results.isEmpty) { - return []; - } - - final matchedResults = - results.mapIndexed((index, d) => toSiblingType(ref, index, d)); - - return Future.wait(matchedResults); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - rethrow; - } - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return DABMusicSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == this.info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, this.info); - - final source = await fetchSources( - sibling.id, - ref.read(userPreferencesProvider).audioQuality, - const AudioQuality( - isHiRes: true, - maximumBitDepth: 16, - maximumSamplingRate: 44.1, - ), - ); - - final database = ref.read(databaseProvider); - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: jsonEncode({ - "info": newSourceInfo.toJson(), - "sources": source.map((s) => s.toJson()).toList(), - }), - sourceType: const Value(SourceType.dabMusic), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return DABMusicSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: source, - info: newSourceInfo, - query: query, - source: AudioSource.dabMusic, - ); - } - - @override - Future refreshStream() async { - // There's no need to refresh the stream for DABMusicSourcedTrack - return this; - } -} diff --git a/lib/services/sourced_track/sources/invidious.dart b/lib/services/sourced_track/sources/invidious.dart deleted file mode 100644 index c5421355..00000000 --- a/lib/services/sourced_track/sources/invidious.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:invidious/invidious.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final invidiousProvider = Provider( - (ref) { - final invidiousInstance = ref.watch( - userPreferencesProvider.select((s) => s.invidiousInstance), - ); - return InvidiousClient(server: invidiousInstance); - }, -); - -class InvidiousSourcedTrack extends SourcedTrack { - InvidiousSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - final invidiousClient = ref.read(invidiousProvider); - - if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.youtube), - ), - ); - - return InvidiousSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source as List, - info: siblings.first.info, - query: query, - source: audioSource, - ); - } else { - final manifest = - await invidiousClient.videos.get(cachedSource.sourceId, local: true); - - return InvidiousSourcedTrack( - ref: ref, - siblings: [], - sources: toSources(manifest), - info: TrackSourceInfo( - id: manifest.videoId, - artists: manifest.author, - pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}", - thumbnail: manifest.videoThumbnails.first.url, - title: manifest.title, - durationMs: Duration(seconds: manifest.lengthSeconds).inMilliseconds, - ), - query: query, - source: audioSource, - ); - } - } - - static List toSources(InvidiousVideoResponse manifest) { - return manifest.adaptiveFormats.map((stream) { - var isWebm = stream.type.contains("audio/webm"); - return TrackSource( - url: stream.url.toString(), - quality: switch (stream.qualityLabel) { - "high" => SourceQualities.high, - "medium" => SourceQualities.medium, - _ => SourceQualities.low, - }, - codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, - bitrate: stream.bitrate, - qualityLabel: - "${isWebm ? "Opus" : "AAC"} • ${stream.bitrate.replaceAll("kbps", "")}kbps " - "• ${isWebm ? "weba" : "m4a"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - InvidiousClient invidiousClient, - ) async { - List? sourceMap; - if (index == 0) { - final manifest = await invidiousClient.videos.get(item.id, local: true); - sourceMap = toSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sourceMap, - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final invidiousClient = ref.read(invidiousProvider); - final preference = ref.read(userPreferencesProvider); - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final searchResults = await invidiousClient.search.list( - searchQuery, - type: InvidiousSearchType.video, - ); - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - return await Future.wait( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchResponse( - result, - preference.searchMode, - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), - ); - } - - final rankedSiblings = YoutubeSourcedTrack.rankResults( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchResponse( - result, - preference.searchMode, - ), - ) - .toList(), - query, - ); - - return await Future.wait( - rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return InvidiousSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final pipedClient = ref.read(invidiousProvider); - - final manifest = - await pipedClient.videos.get(newSourceInfo.id, local: true); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return InvidiousSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: toSources(manifest), - info: newSourceInfo, - query: query, - source: source, - ); - } - - @override - Future refreshStream() async { - final manifest = - await ref.read(invidiousProvider).videos.get(info.id, local: true); - - return InvidiousSourcedTrack( - ref: ref, - siblings: siblings, - sources: toSources(manifest), - info: info, - query: query, - source: source, - ); - } -} diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart deleted file mode 100644 index be78be25..00000000 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:jiosaavn/jiosaavn.dart'; -import 'package:spotube/extensions/string.dart'; - -final jiosaavnClient = JioSaavnClient(); - -class JioSaavnSourcedTrack extends SourcedTrack { - JioSaavnSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - bool weakMatch = false, - }) async { - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - - if (cachedSource == null || - cachedSource.sourceType != SourceType.jiosaavn) { - final siblings = - await fetchSiblings(ref: ref, query: query, weakMatch: weakMatch); - - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.jiosaavn), - ), - ); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - sources: siblings.first.source!, - info: siblings.first.info, - query: query, - source: AudioSource.jiosaavn, - ); - } - - final [item] = - await jiosaavnClient.songs.detailsById([cachedSource.sourceId]); - - final (:info, :source) = toSiblingType(item); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: [], - sources: source!, - query: query, - info: info, - source: AudioSource.jiosaavn, - ); - } - - static SiblingType toSiblingType(SongResponse result) { - final SiblingType sibling = ( - info: TrackSourceInfo( - artists: [ - result.primaryArtists, - if (result.featuredArtists.isNotEmpty) ", ", - result.featuredArtists - ].join("").unescapeHtml(), - durationMs: - Duration(seconds: int.parse(result.duration)).inMilliseconds, - id: result.id, - pageUrl: result.url, - thumbnail: result.image?.last.link ?? "", - title: result.name!.unescapeHtml(), - ), - source: result.downloadUrl!.map((link) { - return TrackSource( - url: link.link, - quality: link.quality == "320kbps" - ? SourceQualities.high - : link.quality == "160kbps" - ? SourceQualities.medium - : SourceQualities.low, - codec: SourceCodecs.m4a, - bitrate: link.quality, - qualityLabel: "AAC • ${link.quality} • MP4 • Stereo", - ); - }).toList() - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - bool weakMatch = false, - }) async { - final searchQuery = SourcedTrack.getSearchTerm(query); - - final SongSearchResponse(:results) = - await jiosaavnClient.search.songs(searchQuery, limit: 20); - - final trackArtistNames = query.artists; - - final matchedResults = results - .where( - (s) { - s.name?.unescapeHtml().contains(query.title) ?? false; - - final sameName = s.name?.unescapeHtml() == query.title; - final artistNames = [ - s.primaryArtists, - if (s.featuredArtists.isNotEmpty) ", ", - s.featuredArtists - ].join("").unescapeHtml(); - final sameArtists = artistNames.split(", ").any( - (artist) => trackArtistNames.any((ar) => artist == ar), - ); - if (weakMatch) { - final containsName = - s.name?.unescapeHtml().contains(query.title) ?? false; - final containsPrimaryArtist = s.primaryArtists - .unescapeHtml() - .contains(trackArtistNames.first); - - return containsName && containsPrimaryArtist; - } - - return sameName && sameArtists; - }, - ) - .map(toSiblingType) - .toList(); - - if (weakMatch && matchedResults.isEmpty) { - return results.map(toSiblingType).toList(); - } - - return matchedResults; - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == this.info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, this.info); - - final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]); - - final (:info, :source) = toSiblingType(item); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: info.id, - sourceType: const Value(SourceType.jiosaavn), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return JioSaavnSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: source!, - info: newSourceInfo, - query: query, - source: AudioSource.jiosaavn, - ); - } - - @override - Future refreshStream() async { - // There's no need to refresh the stream for JioSaavnSourcedTrack - return this; - } -} diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart deleted file mode 100644 index fca6c623..00000000 --- a/lib/services/sourced_track/sources/piped.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final pipedProvider = Provider( - (ref) { - final instance = - ref.watch(userPreferencesProvider.select((s) => s.pipedInstance)); - return PipedClient(instance: instance); - }, -); - -class PipedSourcedTrack extends SourcedTrack { - PipedSourcedTrack({ - required super.ref, - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .getSingleOrNull(); - final preferences = ref.read(userPreferencesProvider); - final pipedClient = ref.read(pipedProvider); - - if (cachedSource == null) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: Value( - preferences.searchMode == SearchMode.youtube - ? SourceType.youtube - : SourceType.youtubeMusic, - ), - ), - ); - - return PipedSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - source: audioSource, - info: siblings.first.info, - query: query, - sources: siblings.first.source!, - ); - } else { - final manifest = await pipedClient.streams(cachedSource.sourceId); - - return PipedSourcedTrack( - ref: ref, - siblings: [], - sources: toSources(manifest), - info: TrackSourceInfo( - id: manifest.id, - artists: manifest.uploader, - pageUrl: "https://www.youtube.com/watch?v=${manifest.id}", - thumbnail: manifest.thumbnailUrl, - title: manifest.title, - durationMs: manifest.duration.inMilliseconds, - ), - query: query, - source: audioSource, - ); - } - } - - static List toSources(PipedStreamResponse manifest) { - return manifest.audioStreams.map((audio) { - final isMp4 = audio.format == PipedAudioStreamFormat.m4a; - return TrackSource( - url: audio.url.toString(), - quality: switch (audio.quality) { - "high" => SourceQualities.high, - "medium" => SourceQualities.medium, - _ => SourceQualities.low, - }, - codec: isMp4 ? SourceCodecs.m4a : SourceCodecs.weba, - bitrate: audio.bitrate.toString(), - qualityLabel: - "${isMp4 ? "AAC" : "Opus"} • ${(audio.bitrate / 1000).floor()}kbps " - "• ${isMp4 ? "m4a" : "weba"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - PipedClient pipedClient, - ) async { - List? sources; - if (index == 0) { - final manifest = await pipedClient.streams(item.id); - sources = toSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sources, - ); - - return sibling; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final pipedClient = ref.read(pipedProvider); - final preference = ref.read(userPreferencesProvider); - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final PipedSearchResult(items: searchResults) = await pipedClient.search( - searchQuery, - preference.searchMode == SearchMode.youtube - ? PipedFilter.videos - : PipedFilter.musicSongs, - ); - - // when falling back to piped API make sure to use the YouTube mode - final isYouTubeMusic = preference.audioSource != AudioSource.piped - ? false - : preference.searchMode == SearchMode.youtubeMusic; - - if (isYouTubeMusic) { - final artists = query.artists; - - return await Future.wait( - searchResults - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result as PipedSearchItemStream, - preference.searchMode, - ), - ) - .sorted((a, b) => b.views.compareTo(a.views)) - .where( - (item) => artists.any( - (artist) => - artist.toLowerCase() == item.channelName.toLowerCase(), - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - return await Future.wait( - searchResults - .whereType() - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result, - preference.searchMode, - ), - ) - .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - final rankedSiblings = YoutubeSourcedTrack.rankResults( - searchResults - .map( - (result) => YoutubeVideoInfo.fromSearchItemStream( - result as PipedSearchItemStream, - preference.searchMode, - ), - ) - .toList(), - query, - ); - - return await Future.wait( - rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return PipedSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - info: info, - query: query, - sources: sources, - ); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final pipedClient = ref.read(pipedProvider); - - final manifest = await pipedClient.streams(newSourceInfo.id); - - final database = ref.read(databaseProvider); - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return PipedSourcedTrack( - ref: ref, - siblings: newSiblings, - sources: toSources(manifest), - info: newSourceInfo, - query: query, - source: source, - ); - } - - @override - Future refreshStream() async { - final manifest = await ref.read(pipedProvider).streams(info.id); - return PipedSourcedTrack( - ref: ref, - siblings: siblings, - info: info, - source: source, - query: query, - sources: toSources(manifest), - ); - } -} diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart deleted file mode 100644 index e3e9dd39..00000000 --- a/lib/services/sourced_track/sources/youtube.dart +++ /dev/null @@ -1,439 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; -import 'package:spotube/services/dio/dio.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/song_link/song_link.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; -import 'package:spotube/services/sourced_track/exceptions.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/utils/service_utils.dart'; -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - -final officialMusicRegex = RegExp( - r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", - caseSensitive: false, -); - -class YoutubeSourcedTrack extends SourcedTrack { - YoutubeSourcedTrack({ - required super.source, - required super.siblings, - required super.info, - required super.query, - required super.sources, - required super.ref, - }); - - static Future fetchFromTrack({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final audioSource = ref.read(userPreferencesProvider).audioSource; - final database = ref.read(databaseProvider); - final cachedSource = await (database.select(database.sourceMatchTable) - ..where((s) => s.trackId.equals(query.id)) - ..limit(1) - ..orderBy([ - (s) => - OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc), - ])) - .get() - .then((s) => s.firstOrNull); - - if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { - final siblings = await fetchSiblings(ref: ref, query: query); - if (siblings.isEmpty) { - throw TrackNotFoundError(query); - } - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: siblings.first.info.id, - sourceType: const Value(SourceType.youtube), - ), - ); - - return YoutubeSourcedTrack( - ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - info: siblings.first.info, - source: audioSource, - sources: siblings.first.source ?? [], - query: query, - ); - } - final (item, manifest) = await ref - .read(youtubeEngineProvider) - .getVideoWithStreamInfo(cachedSource.sourceId); - - final sourcedTrack = YoutubeSourcedTrack( - ref: ref, - siblings: [], - sources: toTrackSources(manifest), - info: TrackSourceInfo( - id: item.id.value, - artists: item.author, - pageUrl: item.url, - thumbnail: item.thumbnails.highResUrl, - title: item.title, - durationMs: item.duration?.inMilliseconds ?? 0, - ), - query: query, - source: audioSource, - ); - - AppLogger.log.i("${query.title}: ${sourcedTrack.url}"); - - return sourcedTrack; - } - - static List toTrackSources(StreamManifest manifest) { - return manifest.audioOnly.map((streamInfo) { - var isWebm = streamInfo.codec.mimeType == "audio/webm"; - return TrackSource( - url: streamInfo.url.toString(), - quality: switch (streamInfo.qualityLabel) { - "medium" => SourceQualities.medium, - "high" => SourceQualities.high, - "low" => SourceQualities.low, - _ => SourceQualities.high, - }, - codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a, - bitrate: streamInfo.bitrate.bitsPerSecond.toString(), - qualityLabel: - "${isWebm ? "Opus" : "AAC"} • ${(streamInfo.bitrate.kiloBitsPerSecond).floor()}kbps " - "• ${isWebm ? "weba" : "m4a"} • Stereo", - ); - }).toList(); - } - - static Future toSiblingType( - int index, - YoutubeVideoInfo item, - dynamic ref, - ) async { - assert(ref is WidgetRef || ref is Ref, "Invalid ref type"); - - List? sourceMap; - if (index == 0) { - final manifest = - await ref.read(youtubeEngineProvider).getStreamManifest(item.id); - sourceMap = toTrackSources(manifest); - } - - final SiblingType sibling = ( - info: TrackSourceInfo( - id: item.id, - artists: item.channelName, - pageUrl: "https://www.youtube.com/watch?v=${item.id}", - thumbnail: item.thumbnailUrl, - title: item.title, - durationMs: item.duration.inMilliseconds, - ), - source: sourceMap, - ); - - return sibling; - } - - static List rankResults( - List results, TrackSourceQuery track) { - return results - .sorted((a, b) => b.views.compareTo(a.views)) - .map((sibling) { - int score = 0; - - for (final artist in track.artists) { - final isSameChannelArtist = - sibling.channelName.toLowerCase() == artist.toLowerCase(); - final channelContainsArtist = sibling.channelName - .toLowerCase() - .contains(artist.toLowerCase()); - - if (isSameChannelArtist || channelContainsArtist) { - score += 1; - } - - final titleContainsArtist = - sibling.title.toLowerCase().contains(artist.toLowerCase()); - - if (titleContainsArtist) { - score += 1; - } - } - - final titleContainsTrackName = - sibling.title.toLowerCase().contains(track.title.toLowerCase()); - - final hasOfficialFlag = - officialMusicRegex.hasMatch(sibling.title.toLowerCase()); - - if (titleContainsTrackName) { - score += 3; - } - - if (hasOfficialFlag) { - score += 1; - } - - if (hasOfficialFlag && titleContainsTrackName) { - score += 2; - } - - return (sibling: sibling, score: score); - }) - .sorted((a, b) => b.score.compareTo(a.score)) - .map((e) => e.sibling) - .toList(); - } - - static Future> fetchFromIsrc({ - required TrackSourceQuery track, - required Ref ref, - }) async { - final isrcResults = []; - final isrc = track.isrc; - if (isrc.isNotEmpty) { - final searchedVideos = - await ref.read(youtubeEngineProvider).searchVideos(isrc.toString()); - if (searchedVideos.isNotEmpty) { - AppLogger.log - .d("${track.title} ISRC $isrc Total ${searchedVideos.length}"); - - final stringBuffer = StringBuffer(); - - final filteredMatches = searchedVideos - .map(YoutubeVideoInfo.fromVideo) - .map((YoutubeVideoInfo videoInfo) { - final ytWords = videoInfo.title - .toLowerCase() - .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') - .split(RegExp(r'\p{Z}+', unicode: true)) - .where((item) => item.isNotEmpty); - final spWords = track.title - .toLowerCase() - .replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '') - .split(RegExp(r'\p{Z}+', unicode: true)) - .where((item) => item.isNotEmpty); - // Single word and duration match with 3 second tolerance - if (ytWords.any((word) => spWords.contains(word)) && - (videoInfo.duration - - Duration(milliseconds: track.durationMs)) - .abs() - .inMilliseconds <= - 3000) { - stringBuffer.writeln( - "ISRC MATCH: ${videoInfo.id} ${videoInfo.title} by ${videoInfo.channelName} ${videoInfo.duration}", - ); - - return videoInfo; - } - return null; - }) - .nonNulls - .toList(); - - AppLogger.log.d(stringBuffer.toString()); - - isrcResults.addAll(filteredMatches); - } - } - return isrcResults; - } - - static Future> fetchSiblings({ - required TrackSourceQuery query, - required Ref ref, - }) async { - final videoResults = []; - - if (query is! SourcedTrack) { - final isrcResults = await fetchFromIsrc( - track: query, - ref: ref, - ); - - videoResults.addAll(isrcResults); - - if (isrcResults.isEmpty) { - AppLogger.log.w("No ISRC results found, falling back to SongLink"); - - final links = await SongLinkService.links(query.id); - - final stringBuffer = links.fold( - StringBuffer(), - (previousValue, element) { - previousValue.writeln( - "SongLink ${query.id} ${element.platform} ${element.url}"); - return previousValue; - }, - ); - - AppLogger.log.d(stringBuffer.toString()); - - final ytLink = links.firstWhereOrNull( - (link) => link.platform == "youtube", - ); - if (ytLink?.url != null) { - try { - videoResults.add( - YoutubeVideoInfo.fromVideo(await ref - .read(youtubeEngineProvider) - .getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)), - ); - } on VideoUnplayableException catch (e, stack) { - // Ignore this error and continue with the search - AppLogger.reportError(e, stack); - } - } else { - AppLogger.log.w("No YouTube link found in SongLink results"); - } - } - } - - final searchQuery = SourcedTrack.getSearchTerm(query); - - final searchResults = - await ref.read(youtubeEngineProvider).searchVideos(searchQuery); - - if (ServiceUtils.onlyContainsEnglish(searchQuery)) { - videoResults - .addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList()); - } else { - videoResults.addAll(rankResults( - searchResults.map(YoutubeVideoInfo.fromVideo).toList(), - query, - )); - } - - final seenIds = {}; - int index = 0; - return await Future.wait( - videoResults.map((videoResult) async { - // Deduplicate results - if (!seenIds.contains(videoResult.id)) { - seenIds.add(videoResult.id); - return await toSiblingType(index++, videoResult, ref); - } - return null; - }), - ).then((s) => s.whereType().toList()); - } - - @override - Future swapWithSibling(TrackSourceInfo sibling) async { - if (sibling.id == info.id) { - return null; - } - - // a sibling source that was fetched from the search results - final isStepSibling = siblings.none((s) => s.id == sibling.id); - - final newSourceInfo = isStepSibling - ? sibling - : siblings.firstWhere((s) => s.id == sibling.id); - - final newSiblings = siblings.where((s) => s.id != sibling.id).toList() - ..insert(0, info); - - final manifest = await ref - .read(youtubeEngineProvider) - .getStreamManifest(newSourceInfo.id); - - final database = ref.read(databaseProvider); - - await database.into(database.sourceMatchTable).insert( - SourceMatchTableCompanion.insert( - trackId: query.id, - sourceId: newSourceInfo.id, - sourceType: const Value(SourceType.youtube), - // Because we're sorting by createdAt in the query - // we have to update it to indicate priority - createdAt: Value(DateTime.now()), - ), - mode: InsertMode.replace, - ); - - return YoutubeSourcedTrack( - ref: ref, - source: source, - siblings: newSiblings, - sources: toTrackSources(manifest), - info: newSourceInfo, - query: query, - ); - } - - @override - Future copyWithSibling() async { - if (siblings.isNotEmpty) { - return this; - } - final fetchedSiblings = await fetchSiblings(ref: ref, query: query); - - return YoutubeSourcedTrack( - ref: ref, - siblings: fetchedSiblings - .where((s) => s.info.id != info.id) - .map((s) => s.info) - .toList(), - source: source, - sources: sources, - info: info, - query: query, - ); - } - - @override - Future refreshStream() async { - List validStreams = []; - - final stringBuffer = StringBuffer(); - for (final source in sources) { - final res = await globalDio.head( - source.url, - options: - Options(validateStatus: (status) => status != null && status < 500), - ); - - stringBuffer.writeln( - "[${query.id}] ${res.statusCode} ${source.quality} ${source.codec} ${source.bitrate}", - ); - - if (res.statusCode! < 400) { - validStreams.add(source); - } - } - - AppLogger.log.d(stringBuffer.toString()); - - if (validStreams.isEmpty) { - final manifest = - await ref.read(youtubeEngineProvider).getStreamManifest(info.id); - - validStreams = toTrackSources(manifest); - } - - final sourcedTrack = YoutubeSourcedTrack( - ref: ref, - siblings: siblings, - source: source, - sources: validStreams, - info: info, - query: query, - ); - - AppLogger.log.i("Refreshing ${query.title}: ${sourcedTrack.url}"); - - return sourcedTrack; - } -} diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index fa58314c..c552f883 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -162,7 +162,6 @@ class YouTubeExplodeEngine implements YouTubeEngine { requireWatchPage: false, ytClients: [ YoutubeApiClient.ios, - YoutubeApiClient.android, YoutubeApiClient.androidVr, ], ); diff --git a/pubspec.lock b/pubspec.lock index 08757d74..8623af4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2888,12 +2888,11 @@ packages: youtube_explode_dart: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: caa3023386dbc10e69c99f49f491148094874671 - url: "https://github.com/Coronon/youtube_explode_dart" - source: git - version: "2.5.2" + name: youtube_explode_dart + sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0" + url: "https://pub.dev" + source: hosted + version: "2.5.3" yt_dlp_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 46273a32..4087bc0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,8 +138,7 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: - git: https://github.com/Coronon/youtube_explode_dart + youtube_explode_dart: ^2.5.3 yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git diff --git a/test/drift/app_db/generated/schema.dart b/test/drift/app_db/generated/schema.dart index 413b4408..76573e49 100644 --- a/test/drift/app_db/generated/schema.dart +++ b/test/drift/app_db/generated/schema.dart @@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; import 'schema_v8.dart' as v8; import 'schema_v9.dart' as v9; +import 'schema_v10.dart' as v10; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v8.DatabaseAtV8(db); case 9: return v9.DatabaseAtV9(db); + case 10: + return v10.DatabaseAtV10(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } diff --git a/test/drift/app_db/generated/schema_v10.dart b/test/drift/app_db/generated/schema_v10.dart new file mode 100644 index 00000000..36cc2a6b --- /dev/null +++ b/test/drift/app_db/generated/schema_v10.dart @@ -0,0 +1,3472 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class AuthenticationTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthenticationTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn cookie = GeneratedColumn( + 'cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn accessToken = GeneratedColumn( + 'access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, cookie, accessToken, expiration]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'authentication_table'; + @override + Set get $primaryKey => {id}; + @override + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!, + accessToken: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + + @override + AuthenticationTable createAlias(String alias) { + return AuthenticationTable(attachedDatabase, alias); + } +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final String cookie; + final String accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['cookie'] = Variable(cookie); + map['access_token'] = Variable(accessToken); + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + String? cookie, + String? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) { + return AuthenticationTableData( + id: data.id.present ? data.id.value : this.id, + cookie: data.cookie.present ? data.cookie.value : this.cookie, + accessToken: + data.accessToken.present ? data.accessToken.value : this.accessToken, + expiration: + data.expiration.present ? data.expiration.value : this.expiration, + ); + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, cookie, accessToken, expiration); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required String cookie, + required String accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable(cookie.value); + } + if (accessToken.present) { + map['access_token'] = Variable(accessToken.value); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class BlacklistTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BlacklistTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementType = GeneratedColumn( + 'element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_table'; + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_type'])!, + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + BlacklistTable createAlias(String alias) { + return BlacklistTable(attachedDatabase, alias); + } +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['element_type'] = Variable(elementType); + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: serializer.fromJson(json['elementType']), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson(elementType), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, String? name, String? elementType, String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + BlacklistTableData copyWithCompanion(BlacklistTableCompanion data) { + return BlacklistTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + elementType: + data.elementType.present ? data.elementType.value : this.elementType, + elementId: data.elementId.present ? data.elementId.value : this.elementId, + ); + } + + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable(elementType.value); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + +class PreferencesTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PreferencesTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)); + late final GeneratedColumn accentColorScheme = + GeneratedColumn('accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Slate:0xff64748b")); + late final GeneratedColumn layoutMode = GeneratedColumn( + 'layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)); + late final GeneratedColumn locale = GeneratedColumn( + 'locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: + const Constant('{"languageCode":"system","countryCode":"system"}')); + late final GeneratedColumn market = GeneratedColumn( + 'market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)); + late final GeneratedColumn searchMode = GeneratedColumn( + 'search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)); + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn localLibraryLocation = + GeneratedColumn('local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + late final GeneratedColumn invidiousInstance = + GeneratedColumn('invidious_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://inv.nadeko.net")); + late final GeneratedColumn themeMode = GeneratedColumn( + 'theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)); + late final GeneratedColumn audioSourceId = GeneratedColumn( + 'audio_source_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn youtubeClientEngine = + GeneratedColumn('youtube_client_engine', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn connectPort = GeneratedColumn( + 'connect_port', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(-1)); + late final GeneratedColumn cacheMusic = GeneratedColumn( + 'cache_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("cache_music" IN (0, 1))'), + defaultValue: const Constant(true)); + @override + List get $columns => [ + id, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSourceId, + youtubeClientEngine, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_table'; + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}close_behavior'])!, + accentColorScheme: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}accent_color_scheme'])!, + layoutMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}layout_mode'])!, + locale: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!, + market: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!, + searchMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}search_mode'])!, + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!, + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + invidiousInstance: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, + themeMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, + audioSourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source_id']), + youtubeClientEngine: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}youtube_client_engine'])!, + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + connectPort: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}connect_port'])!, + cacheMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}cache_music'])!, + ); + } + + @override + PreferencesTable createAlias(String alias) { + return PreferencesTable(attachedDatabase, alias); + } +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final String closeBehavior; + final String accentColorScheme; + final String layoutMode; + final String locale; + final String market; + final String searchMode; + final String downloadLocation; + final String localLibraryLocation; + final String pipedInstance; + final String invidiousInstance; + final String themeMode; + final String? audioSourceId; + final String youtubeClientEngine; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + final int connectPort; + final bool cacheMusic; + const PreferencesTableData( + {required this.id, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.invidiousInstance, + required this.themeMode, + this.audioSourceId, + required this.youtubeClientEngine, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect, + required this.connectPort, + required this.cacheMusic}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + map['close_behavior'] = Variable(closeBehavior); + map['accent_color_scheme'] = Variable(accentColorScheme); + map['layout_mode'] = Variable(layoutMode); + map['locale'] = Variable(locale); + map['market'] = Variable(market); + map['search_mode'] = Variable(searchMode); + map['download_location'] = Variable(downloadLocation); + map['local_library_location'] = Variable(localLibraryLocation); + map['piped_instance'] = Variable(pipedInstance); + map['invidious_instance'] = Variable(invidiousInstance); + map['theme_mode'] = Variable(themeMode); + if (!nullToAbsent || audioSourceId != null) { + map['audio_source_id'] = Variable(audioSourceId); + } + map['youtube_client_engine'] = Variable(youtubeClientEngine); + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + map['connect_port'] = Variable(connectPort); + map['cache_music'] = Variable(cacheMusic); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + invidiousInstance: Value(invidiousInstance), + themeMode: Value(themeMode), + audioSourceId: audioSourceId == null && nullToAbsent + ? const Value.absent() + : Value(audioSourceId), + youtubeClientEngine: Value(youtubeClientEngine), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + connectPort: Value(connectPort), + cacheMusic: Value(cacheMusic), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: serializer.fromJson(json['closeBehavior']), + accentColorScheme: serializer.fromJson(json['accentColorScheme']), + layoutMode: serializer.fromJson(json['layoutMode']), + locale: serializer.fromJson(json['locale']), + market: serializer.fromJson(json['market']), + searchMode: serializer.fromJson(json['searchMode']), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + invidiousInstance: serializer.fromJson(json['invidiousInstance']), + themeMode: serializer.fromJson(json['themeMode']), + audioSourceId: serializer.fromJson(json['audioSourceId']), + youtubeClientEngine: + serializer.fromJson(json['youtubeClientEngine']), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + connectPort: serializer.fromJson(json['connectPort']), + cacheMusic: serializer.fromJson(json['cacheMusic']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson(closeBehavior), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson(layoutMode), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson(market), + 'searchMode': serializer.toJson(searchMode), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': serializer.toJson(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'invidiousInstance': serializer.toJson(invidiousInstance), + 'themeMode': serializer.toJson(themeMode), + 'audioSourceId': serializer.toJson(audioSourceId), + 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + 'connectPort': serializer.toJson(connectPort), + 'cacheMusic': serializer.toJson(cacheMusic), + }; + } + + PreferencesTableData copyWith( + {int? id, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + String? closeBehavior, + String? accentColorScheme, + String? layoutMode, + String? locale, + String? market, + String? searchMode, + String? downloadLocation, + String? localLibraryLocation, + String? pipedInstance, + String? invidiousInstance, + String? themeMode, + Value audioSourceId = const Value.absent(), + String? youtubeClientEngine, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect, + int? connectPort, + bool? cacheMusic}) => + PreferencesTableData( + id: id ?? this.id, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSourceId: + audioSourceId.present ? audioSourceId.value : this.audioSourceId, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { + return PreferencesTableData( + id: data.id.present ? data.id.value : this.id, + albumColorSync: data.albumColorSync.present + ? data.albumColorSync.value + : this.albumColorSync, + amoledDarkTheme: data.amoledDarkTheme.present + ? data.amoledDarkTheme.value + : this.amoledDarkTheme, + checkUpdate: + data.checkUpdate.present ? data.checkUpdate.value : this.checkUpdate, + normalizeAudio: data.normalizeAudio.present + ? data.normalizeAudio.value + : this.normalizeAudio, + showSystemTrayIcon: data.showSystemTrayIcon.present + ? data.showSystemTrayIcon.value + : this.showSystemTrayIcon, + systemTitleBar: data.systemTitleBar.present + ? data.systemTitleBar.value + : this.systemTitleBar, + skipNonMusic: data.skipNonMusic.present + ? data.skipNonMusic.value + : this.skipNonMusic, + closeBehavior: data.closeBehavior.present + ? data.closeBehavior.value + : this.closeBehavior, + accentColorScheme: data.accentColorScheme.present + ? data.accentColorScheme.value + : this.accentColorScheme, + layoutMode: + data.layoutMode.present ? data.layoutMode.value : this.layoutMode, + locale: data.locale.present ? data.locale.value : this.locale, + market: data.market.present ? data.market.value : this.market, + searchMode: + data.searchMode.present ? data.searchMode.value : this.searchMode, + downloadLocation: data.downloadLocation.present + ? data.downloadLocation.value + : this.downloadLocation, + localLibraryLocation: data.localLibraryLocation.present + ? data.localLibraryLocation.value + : this.localLibraryLocation, + pipedInstance: data.pipedInstance.present + ? data.pipedInstance.value + : this.pipedInstance, + invidiousInstance: data.invidiousInstance.present + ? data.invidiousInstance.value + : this.invidiousInstance, + themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, + audioSourceId: data.audioSourceId.present + ? data.audioSourceId.value + : this.audioSourceId, + youtubeClientEngine: data.youtubeClientEngine.present + ? data.youtubeClientEngine.value + : this.youtubeClientEngine, + discordPresence: data.discordPresence.present + ? data.discordPresence.value + : this.discordPresence, + endlessPlayback: data.endlessPlayback.present + ? data.endlessPlayback.value + : this.endlessPlayback, + enableConnect: data.enableConnect.present + ? data.enableConnect.value + : this.enableConnect, + connectPort: + data.connectPort.present ? data.connectPort.value : this.connectPort, + cacheMusic: + data.cacheMusic.present ? data.cacheMusic.value : this.cacheMusic, + ); + } + + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSourceId: $audioSourceId, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + invidiousInstance, + themeMode, + audioSourceId, + youtubeClientEngine, + discordPresence, + endlessPlayback, + enableConnect, + connectPort, + cacheMusic + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.invidiousInstance == this.invidiousInstance && + other.themeMode == this.themeMode && + other.audioSourceId == this.audioSourceId && + other.youtubeClientEngine == this.youtubeClientEngine && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect && + other.connectPort == this.connectPort && + other.cacheMusic == this.cacheMusic); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value localLibraryLocation; + final Value pipedInstance; + final Value invidiousInstance; + final Value themeMode; + final Value audioSourceId; + final Value youtubeClientEngine; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + final Value connectPort; + final Value cacheMusic; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSourceId = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.invidiousInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSourceId = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + this.connectPort = const Value.absent(), + this.cacheMusic = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? invidiousInstance, + Expression? themeMode, + Expression? audioSourceId, + Expression? youtubeClientEngine, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + Expression? connectPort, + Expression? cacheMusic, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (invidiousInstance != null) 'invidious_instance': invidiousInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSourceId != null) 'audio_source_id': audioSourceId, + if (youtubeClientEngine != null) + 'youtube_client_engine': youtubeClientEngine, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + if (connectPort != null) 'connect_port': connectPort, + if (cacheMusic != null) 'cache_music': cacheMusic, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value? localLibraryLocation, + Value? pipedInstance, + Value? invidiousInstance, + Value? themeMode, + Value? audioSourceId, + Value? youtubeClientEngine, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect, + Value? connectPort, + Value? cacheMusic}) { + return PreferencesTableCompanion( + id: id ?? this.id, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + invidiousInstance: invidiousInstance ?? this.invidiousInstance, + themeMode: themeMode ?? this.themeMode, + audioSourceId: audioSourceId ?? this.audioSourceId, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + connectPort: connectPort ?? this.connectPort, + cacheMusic: cacheMusic ?? this.cacheMusic, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable(closeBehavior.value); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable(accentColorScheme.value); + } + if (layoutMode.present) { + map['layout_mode'] = Variable(layoutMode.value); + } + if (locale.present) { + map['locale'] = Variable(locale.value); + } + if (market.present) { + map['market'] = Variable(market.value); + } + if (searchMode.present) { + map['search_mode'] = Variable(searchMode.value); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = + Variable(localLibraryLocation.value); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (invidiousInstance.present) { + map['invidious_instance'] = Variable(invidiousInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable(themeMode.value); + } + if (audioSourceId.present) { + map['audio_source_id'] = Variable(audioSourceId.value); + } + if (youtubeClientEngine.present) { + map['youtube_client_engine'] = + Variable(youtubeClientEngine.value); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + if (connectPort.present) { + map['connect_port'] = Variable(connectPort.value); + } + if (cacheMusic.present) { + map['cache_music'] = Variable(cacheMusic.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('invidiousInstance: $invidiousInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSourceId: $audioSourceId, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect, ') + ..write('connectPort: $connectPort, ') + ..write('cacheMusic: $cacheMusic') + ..write(')')) + .toString(); + } +} + +class ScrobblerTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ScrobblerTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + ScrobblerTable createAlias(String alias) { + return ScrobblerTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + ScrobblerTableData copyWithCompanion(ScrobblerTableCompanion data) { + return ScrobblerTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + username: data.username.present ? data.username.value : this.username, + passwordHash: data.passwordHash.present + ? data.passwordHash.value + : this.passwordHash, + ); + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + +class SkipSegmentTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SkipSegmentTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_table'; + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SkipSegmentTable createAlias(String alias) { + return SkipSegmentTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + SkipSegmentTableData copyWithCompanion(SkipSegmentTableCompanion data) { + return SkipSegmentTableData( + id: data.id.present ? data.id.value : this.id, + start: data.start.present ? data.start.value : this.start, + end: data.end.present ? data.end.value : this.end, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SourceMatchTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SourceMatchTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceInfo = GeneratedColumn( + 'source_info', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceInfo, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_table'; + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_info'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + SourceMatchTable createAlias(String alias) { + return SourceMatchTable(attachedDatabase, alias); + } +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceInfo; + final String sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceInfo, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_info'] = Variable(sourceInfo); + map['source_type'] = Variable(sourceType); + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceInfo: Value(sourceInfo), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceInfo: serializer.fromJson(json['sourceInfo']), + sourceType: serializer.fromJson(json['sourceType']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceInfo': serializer.toJson(sourceInfo), + 'sourceType': serializer.toJson(sourceType), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceInfo, + String? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceInfo: sourceInfo ?? this.sourceInfo, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + SourceMatchTableData copyWithCompanion(SourceMatchTableCompanion data) { + return SourceMatchTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + sourceInfo: + data.sourceInfo.present ? data.sourceInfo.value : this.sourceInfo, + sourceType: + data.sourceType.present ? data.sourceType.value : this.sourceType, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceInfo: $sourceInfo, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, trackId, sourceInfo, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceInfo == this.sourceInfo && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceInfo; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceInfo = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceInfo, + required String sourceType, + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceInfo = Value(sourceInfo), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceInfo, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceInfo != null) 'source_info': sourceInfo, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceInfo, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceInfo: sourceInfo ?? this.sourceInfo, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (sourceInfo.present) { + map['source_info'] = Variable(sourceInfo.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceInfo: $sourceInfo, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class AudioPlayerStateTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AudioPlayerStateTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + late final GeneratedColumn loopMode = GeneratedColumn( + 'loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + late final GeneratedColumn collections = GeneratedColumn( + 'collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn tracks = GeneratedColumn( + 'tracks', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("[]")); + late final GeneratedColumn currentIndex = GeneratedColumn( + 'current_index', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + List get $columns => + [id, playing, loopMode, shuffled, collections, tracks, currentIndex]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_table'; + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + loopMode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!, + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}collections'])!, + tracks: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}tracks'])!, + currentIndex: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}current_index'])!, + ); + } + + @override + AudioPlayerStateTable createAlias(String alias) { + return AudioPlayerStateTable(attachedDatabase, alias); + } +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final String loopMode; + final bool shuffled; + final String collections; + final String tracks; + final int currentIndex; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.loopMode, + required this.shuffled, + required this.collections, + required this.tracks, + required this.currentIndex}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['loop_mode'] = Variable(loopMode); + map['shuffled'] = Variable(shuffled); + map['collections'] = Variable(collections); + map['tracks'] = Variable(tracks); + map['current_index'] = Variable(currentIndex); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + collections: Value(collections), + tracks: Value(tracks), + currentIndex: Value(currentIndex), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + loopMode: serializer.fromJson(json['loopMode']), + shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson(json['collections']), + tracks: serializer.fromJson(json['tracks']), + currentIndex: serializer.fromJson(json['currentIndex']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'loopMode': serializer.toJson(loopMode), + 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson(collections), + 'tracks': serializer.toJson(tracks), + 'currentIndex': serializer.toJson(currentIndex), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + String? loopMode, + bool? shuffled, + String? collections, + String? tracks, + int? currentIndex}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + AudioPlayerStateTableData copyWithCompanion( + AudioPlayerStateTableCompanion data) { + return AudioPlayerStateTableData( + id: data.id.present ? data.id.value : this.id, + playing: data.playing.present ? data.playing.value : this.playing, + loopMode: data.loopMode.present ? data.loopMode.value : this.loopMode, + shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled, + collections: + data.collections.present ? data.collections.value : this.collections, + tracks: data.tracks.present ? data.tracks.value : this.tracks, + currentIndex: data.currentIndex.present + ? data.currentIndex.value + : this.currentIndex, + ); + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, playing, loopMode, shuffled, collections, tracks, currentIndex); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled && + other.collections == this.collections && + other.tracks == this.tracks && + other.currentIndex == this.currentIndex); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value loopMode; + final Value shuffled; + final Value collections; + final Value tracks; + final Value currentIndex; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + this.collections = const Value.absent(), + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required String loopMode, + required bool shuffled, + required String collections, + this.tracks = const Value.absent(), + this.currentIndex = const Value.absent(), + }) : playing = Value(playing), + loopMode = Value(loopMode), + shuffled = Value(shuffled), + collections = Value(collections); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? loopMode, + Expression? shuffled, + Expression? collections, + Expression? tracks, + Expression? currentIndex, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, + if (tracks != null) 'tracks': tracks, + if (currentIndex != null) 'current_index': currentIndex, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? loopMode, + Value? shuffled, + Value? collections, + Value? tracks, + Value? currentIndex}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, + tracks: tracks ?? this.tracks, + currentIndex: currentIndex ?? this.currentIndex, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable(loopMode.value); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + if (collections.present) { + map['collections'] = Variable(collections.value); + } + if (tracks.present) { + map['tracks'] = Variable(tracks.value); + } + if (currentIndex.present) { + map['current_index'] = Variable(currentIndex.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections, ') + ..write('tracks: $tracks, ') + ..write('currentIndex: $currentIndex') + ..write(')')) + .toString(); + } +} + +class HistoryTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + HistoryTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + HistoryTable createAlias(String alias) { + return HistoryTable(attachedDatabase, alias); + } +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String type; + final String itemId; + final String data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['type'] = Variable(type); + map['item_id'] = Variable(itemId); + map['data'] = Variable(data); + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: serializer.fromJson(json['type']), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer.toJson(type), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + String? type, + String? itemId, + String? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + HistoryTableData copyWithCompanion(HistoryTableCompanion data) { + return HistoryTableData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + type: data.type.present ? data.type.value : this.type, + itemId: data.itemId.present ? data.itemId.value : this.itemId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String type, + required String itemId, + required String data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class LyricsTable extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LyricsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn data = GeneratedColumn( + 'data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, trackId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'lyrics_table'; + @override + Set get $primaryKey => {id}; + @override + LyricsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LyricsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + data: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!, + ); + } + + @override + LyricsTable createAlias(String alias) { + return LyricsTable(attachedDatabase, alias); + } +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final String data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['data'] = Variable(data); + return map; + } + + LyricsTableCompanion toCompanion(bool nullToAbsent) { + return LyricsTableCompanion( + id: Value(id), + trackId: Value(trackId), + data: Value(data), + ); + } + + factory LyricsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(data), + }; + } + + LyricsTableData copyWith({int? id, String? trackId, String? data}) => + LyricsTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + LyricsTableData copyWithCompanion(LyricsTableCompanion data) { + return LyricsTableData( + id: data.id.present ? data.id.value : this.id, + trackId: data.trackId.present ? data.trackId.value : this.trackId, + data: data.data.present ? data.data.value : this.data, + ); + } + + @override + String toString() { + return (StringBuffer('LyricsTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LyricsTableData && + other.id == this.id && + other.trackId == this.trackId && + other.data == this.data); +} + +class LyricsTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value data; + const LyricsTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.data = const Value.absent(), + }); + LyricsTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String data, + }) : trackId = Value(trackId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LyricsTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + +class PluginsTable extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PluginsTable(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn author = GeneratedColumn( + 'author', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn entryPoint = GeneratedColumn( + 'entry_point', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn apis = GeneratedColumn( + 'apis', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn abilities = GeneratedColumn( + 'abilities', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn selectedForMetadata = GeneratedColumn( + 'selected_for_metadata', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_metadata" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn selectedForAudioSource = + GeneratedColumn('selected_for_audio_source', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected_for_audio_source" IN (0, 1))'), + defaultValue: const Constant(false)); + late final GeneratedColumn repository = GeneratedColumn( + 'repository', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn pluginApiVersion = GeneratedColumn( + 'plugin_api_version', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('2.0.0')); + @override + List get $columns => [ + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'plugins_table'; + @override + Set get $primaryKey => {id}; + @override + PluginsTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PluginsTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + version: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}version'])!, + author: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}author'])!, + entryPoint: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}entry_point'])!, + apis: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}apis'])!, + abilities: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}abilities'])!, + selectedForMetadata: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}selected_for_metadata'])!, + selectedForAudioSource: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected_for_audio_source'])!, + repository: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}repository']), + pluginApiVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}plugin_api_version'])!, + ); + } + + @override + PluginsTable createAlias(String alias) { + return PluginsTable(attachedDatabase, alias); + } +} + +class PluginsTableData extends DataClass + implements Insertable { + final int id; + final String name; + final String description; + final String version; + final String author; + final String entryPoint; + final String apis; + final String abilities; + final bool selectedForMetadata; + final bool selectedForAudioSource; + final String? repository; + final String pluginApiVersion; + const PluginsTableData( + {required this.id, + required this.name, + required this.description, + required this.version, + required this.author, + required this.entryPoint, + required this.apis, + required this.abilities, + required this.selectedForMetadata, + required this.selectedForAudioSource, + this.repository, + required this.pluginApiVersion}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['version'] = Variable(version); + map['author'] = Variable(author); + map['entry_point'] = Variable(entryPoint); + map['apis'] = Variable(apis); + map['abilities'] = Variable(abilities); + map['selected_for_metadata'] = Variable(selectedForMetadata); + map['selected_for_audio_source'] = Variable(selectedForAudioSource); + if (!nullToAbsent || repository != null) { + map['repository'] = Variable(repository); + } + map['plugin_api_version'] = Variable(pluginApiVersion); + return map; + } + + PluginsTableCompanion toCompanion(bool nullToAbsent) { + return PluginsTableCompanion( + id: Value(id), + name: Value(name), + description: Value(description), + version: Value(version), + author: Value(author), + entryPoint: Value(entryPoint), + apis: Value(apis), + abilities: Value(abilities), + selectedForMetadata: Value(selectedForMetadata), + selectedForAudioSource: Value(selectedForAudioSource), + repository: repository == null && nullToAbsent + ? const Value.absent() + : Value(repository), + pluginApiVersion: Value(pluginApiVersion), + ); + } + + factory PluginsTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PluginsTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + version: serializer.fromJson(json['version']), + author: serializer.fromJson(json['author']), + entryPoint: serializer.fromJson(json['entryPoint']), + apis: serializer.fromJson(json['apis']), + abilities: serializer.fromJson(json['abilities']), + selectedForMetadata: + serializer.fromJson(json['selectedForMetadata']), + selectedForAudioSource: + serializer.fromJson(json['selectedForAudioSource']), + repository: serializer.fromJson(json['repository']), + pluginApiVersion: serializer.fromJson(json['pluginApiVersion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'version': serializer.toJson(version), + 'author': serializer.toJson(author), + 'entryPoint': serializer.toJson(entryPoint), + 'apis': serializer.toJson(apis), + 'abilities': serializer.toJson(abilities), + 'selectedForMetadata': serializer.toJson(selectedForMetadata), + 'selectedForAudioSource': serializer.toJson(selectedForAudioSource), + 'repository': serializer.toJson(repository), + 'pluginApiVersion': serializer.toJson(pluginApiVersion), + }; + } + + PluginsTableData copyWith( + {int? id, + String? name, + String? description, + String? version, + String? author, + String? entryPoint, + String? apis, + String? abilities, + bool? selectedForMetadata, + bool? selectedForAudioSource, + Value repository = const Value.absent(), + String? pluginApiVersion}) => + PluginsTableData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository.present ? repository.value : this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + PluginsTableData copyWithCompanion(PluginsTableCompanion data) { + return PluginsTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + version: data.version.present ? data.version.value : this.version, + author: data.author.present ? data.author.value : this.author, + entryPoint: + data.entryPoint.present ? data.entryPoint.value : this.entryPoint, + apis: data.apis.present ? data.apis.value : this.apis, + abilities: data.abilities.present ? data.abilities.value : this.abilities, + selectedForMetadata: data.selectedForMetadata.present + ? data.selectedForMetadata.value + : this.selectedForMetadata, + selectedForAudioSource: data.selectedForAudioSource.present + ? data.selectedForAudioSource.value + : this.selectedForAudioSource, + repository: + data.repository.present ? data.repository.value : this.repository, + pluginApiVersion: data.pluginApiVersion.present + ? data.pluginApiVersion.value + : this.pluginApiVersion, + ); + } + + @override + String toString() { + return (StringBuffer('PluginsTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + version, + author, + entryPoint, + apis, + abilities, + selectedForMetadata, + selectedForAudioSource, + repository, + pluginApiVersion); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PluginsTableData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.version == this.version && + other.author == this.author && + other.entryPoint == this.entryPoint && + other.apis == this.apis && + other.abilities == this.abilities && + other.selectedForMetadata == this.selectedForMetadata && + other.selectedForAudioSource == this.selectedForAudioSource && + other.repository == this.repository && + other.pluginApiVersion == this.pluginApiVersion); +} + +class PluginsTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value version; + final Value author; + final Value entryPoint; + final Value apis; + final Value abilities; + final Value selectedForMetadata; + final Value selectedForAudioSource; + final Value repository; + final Value pluginApiVersion; + const PluginsTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.version = const Value.absent(), + this.author = const Value.absent(), + this.entryPoint = const Value.absent(), + this.apis = const Value.absent(), + this.abilities = const Value.absent(), + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }); + PluginsTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String description, + required String version, + required String author, + required String entryPoint, + required String apis, + required String abilities, + this.selectedForMetadata = const Value.absent(), + this.selectedForAudioSource = const Value.absent(), + this.repository = const Value.absent(), + this.pluginApiVersion = const Value.absent(), + }) : name = Value(name), + description = Value(description), + version = Value(version), + author = Value(author), + entryPoint = Value(entryPoint), + apis = Value(apis), + abilities = Value(abilities); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? version, + Expression? author, + Expression? entryPoint, + Expression? apis, + Expression? abilities, + Expression? selectedForMetadata, + Expression? selectedForAudioSource, + Expression? repository, + Expression? pluginApiVersion, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (version != null) 'version': version, + if (author != null) 'author': author, + if (entryPoint != null) 'entry_point': entryPoint, + if (apis != null) 'apis': apis, + if (abilities != null) 'abilities': abilities, + if (selectedForMetadata != null) + 'selected_for_metadata': selectedForMetadata, + if (selectedForAudioSource != null) + 'selected_for_audio_source': selectedForAudioSource, + if (repository != null) 'repository': repository, + if (pluginApiVersion != null) 'plugin_api_version': pluginApiVersion, + }); + } + + PluginsTableCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value? version, + Value? author, + Value? entryPoint, + Value? apis, + Value? abilities, + Value? selectedForMetadata, + Value? selectedForAudioSource, + Value? repository, + Value? pluginApiVersion}) { + return PluginsTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + version: version ?? this.version, + author: author ?? this.author, + entryPoint: entryPoint ?? this.entryPoint, + apis: apis ?? this.apis, + abilities: abilities ?? this.abilities, + selectedForMetadata: selectedForMetadata ?? this.selectedForMetadata, + selectedForAudioSource: + selectedForAudioSource ?? this.selectedForAudioSource, + repository: repository ?? this.repository, + pluginApiVersion: pluginApiVersion ?? this.pluginApiVersion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (version.present) { + map['version'] = Variable(version.value); + } + if (author.present) { + map['author'] = Variable(author.value); + } + if (entryPoint.present) { + map['entry_point'] = Variable(entryPoint.value); + } + if (apis.present) { + map['apis'] = Variable(apis.value); + } + if (abilities.present) { + map['abilities'] = Variable(abilities.value); + } + if (selectedForMetadata.present) { + map['selected_for_metadata'] = Variable(selectedForMetadata.value); + } + if (selectedForAudioSource.present) { + map['selected_for_audio_source'] = + Variable(selectedForAudioSource.value); + } + if (repository.present) { + map['repository'] = Variable(repository.value); + } + if (pluginApiVersion.present) { + map['plugin_api_version'] = Variable(pluginApiVersion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PluginsTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('version: $version, ') + ..write('author: $author, ') + ..write('entryPoint: $entryPoint, ') + ..write('apis: $apis, ') + ..write('abilities: $abilities, ') + ..write('selectedForMetadata: $selectedForMetadata, ') + ..write('selectedForAudioSource: $selectedForAudioSource, ') + ..write('repository: $repository, ') + ..write('pluginApiVersion: $pluginApiVersion') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV10 extends GeneratedDatabase { + DatabaseAtV10(QueryExecutor e) : super(e); + late final AuthenticationTable authenticationTable = + AuthenticationTable(this); + late final BlacklistTable blacklistTable = BlacklistTable(this); + late final PreferencesTable preferencesTable = PreferencesTable(this); + late final ScrobblerTable scrobblerTable = ScrobblerTable(this); + late final SkipSegmentTable skipSegmentTable = SkipSegmentTable(this); + late final SourceMatchTable sourceMatchTable = SourceMatchTable(this); + late final AudioPlayerStateTable audioPlayerStateTable = + AudioPlayerStateTable(this); + late final HistoryTable historyTable = HistoryTable(this); + late final LyricsTable lyricsTable = LyricsTable(this); + late final PluginsTable pluginsTable = PluginsTable(this); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch + ]; + @override + int get schemaVersion => 10; +} From 63118319021e5b9e35b023792122345aad74e628 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 19:33:47 +0600 Subject: [PATCH 19/47] feat: move away from track source query and preferences audio quality and codec --- drift_schemas/app_db/drift_schema_v10.json | 1 + .../dialogs/track_details_dialog.dart | 11 +- .../presentation_actions.dart | 19 +- lib/models/database/database.dart | 9 + lib/models/database/database.g.dart | 602 +++---------- lib/models/database/database.steps.dart | 307 ++++++- lib/models/database/tables/preferences.dart | 6 - lib/models/database/tables/source_match.dart | 2 +- lib/models/metadata/audio_source.dart | 22 +- lib/models/metadata/metadata.dart | 1 + lib/models/metadata/metadata.freezed.dart | 62 +- lib/models/metadata/metadata.g.dart | 4 +- lib/models/playback/track_sources.dart | 1 - .../playback/track_sources.freezed.dart | 800 ------------------ lib/models/playback/track_sources.g.dart | 99 +-- .../local_folder/cache_export_dialog.dart | 5 +- lib/modules/player/player.dart | 56 +- lib/modules/player/sibling_tracks_sheet.dart | 347 ++------ .../getting_started/sections/playback.dart | 123 ++- lib/pages/settings/sections/playback.dart | 439 ++-------- lib/provider/audio_player/audio_player.dart | 11 +- .../audio_player/audio_player_streams.dart | 7 +- .../audio_player/querying_track_info.dart | 12 +- .../sources/invidious_instances_provider.dart | 12 - .../sources/piped_instances_provider.dart | 17 - lib/provider/download_manager_provider.dart | 71 +- .../audio_source/quality_label.dart | 12 + .../audio_source/quality_presets.dart | 120 +++ .../audio_source/quality_presets.freezed.dart | 289 +++++++ .../audio_source/quality_presets.g.dart | 38 + lib/provider/server/active_track_sources.dart | 17 +- lib/provider/server/routes/playback.dart | 49 +- .../server/sourced_track_provider.dart | 49 ++ lib/provider/server/track_sources.dart | 48 -- lib/provider/skip_segments/skip_segments.dart | 14 +- .../user_preferences_provider.dart | 61 -- lib/services/audio_player/audio_player.dart | 25 +- lib/services/sourced_track/exceptions.dart | 6 +- lib/services/sourced_track/sourced_track.dart | 55 +- lib/utils/service_utils.dart | 91 -- pubspec.lock | 32 - pubspec.yaml | 3 - test/drift/app_db/generated/schema_v10.dart | 73 +- 43 files changed, 1352 insertions(+), 2676 deletions(-) create mode 100644 drift_schemas/app_db/drift_schema_v10.json delete mode 100644 lib/models/playback/track_sources.freezed.dart delete mode 100644 lib/provider/audio_player/sources/invidious_instances_provider.dart delete mode 100644 lib/provider/audio_player/sources/piped_instances_provider.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_label.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart create mode 100644 lib/provider/metadata_plugin/audio_source/quality_presets.g.dart create mode 100644 lib/provider/server/sourced_track_provider.dart delete mode 100644 lib/provider/server/track_sources.dart diff --git a/drift_schemas/app_db/drift_schema_v10.json b/drift_schemas/app_db/drift_schema_v10.json new file mode 100644 index 00000000..5fb86d25 --- /dev/null +++ b/drift_schemas/app_db/drift_schema_v10.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Slate:0xff64748b\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source_id","getter_name":"audioSourceId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_info","getter_name":"sourceInfo","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"{}\")","default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected_for_metadata","getter_name":"selectedForMetadata","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_metadata\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_metadata\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"selected_for_audio_source","getter_name":"selectedForAudioSource","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected_for_audio_source\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected_for_audio_source\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('2.0.0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_info","source_type"]}}]} \ No newline at end of file diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 3d3fd7e9..9d35a6fb 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -7,8 +7,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; class TrackDetailsDialog extends HookConsumerWidget { final SpotubeFullTrackObject track; @@ -21,8 +20,7 @@ class TrackDetailsDialog extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final sourcedTrack = - ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track))); + final sourcedTrack = ref.read(sourcedTrackProvider(track)); final detailsMap = { context.l10n.title: track.name, @@ -39,8 +37,7 @@ class TrackDetailsDialog extends HookConsumerWidget { // style: const TextStyle(color: Colors.blue), // ), context.l10n.duration: sourcedTrack.asData != null - ? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs) - .toHumanReadableString() + ? sourcedTrack.asData!.value.info.duration.toHumanReadableString() : Duration(milliseconds: track.durationMs).toHumanReadableString(), if (track.album.releaseDate != null) context.l10n.released: track.album.releaseDate, @@ -57,7 +54,7 @@ class TrackDetailsDialog extends HookConsumerWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - context.l10n.channel: Text(sourceInfo.artists), + context.l10n.channel: Text(sourceInfo.artists.join(", ")), if (sourcedTrack.asData?.value.url != null) context.l10n.streamUrl: Hyperlink( sourcedTrack.asData!.value.url ?? "", diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart index 735a4514..54aa3428 100644 --- a/lib/components/track_presentation/presentation_actions.dart +++ b/lib/components/track_presentation/presentation_actions.dart @@ -8,12 +8,10 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; ToastOverlay showToastForAction( BuildContext context, @@ -70,8 +68,6 @@ class TrackPresentationActionsSection extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryActionsProvider); - final audioSource = - ref.watch(userPreferencesProvider.select((s) => s.audioSource)); final state = ref.watch(presentationStateProvider(options.collection)); final notifier = @@ -85,14 +81,13 @@ class TrackPresentationActionsSection extends HookConsumerWidget { }) async { final fullTrackObjects = tracks.whereType().toList(); - final confirmed = audioSource == AudioSource.piped || - (await showDialog( - context: context, - builder: (context) { - return const ConfirmDownloadDialog(); - }, - ) ?? - false); + final confirmed = await showDialog( + context: context, + builder: (context) { + return const ConfirmDownloadDialog(); + }, + ) ?? + false; if (confirmed != true) return; downloader.batchAddToQueue(fullTrackObjects); notifier.deselectAllTracks(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index a03cdb8c..786b813f 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -210,6 +210,15 @@ class AppDatabase extends _$AppDatabase { pluginsTable.selectedForAudioSource, ); }, + from9To10: (m, schema) async { + await m.dropColumn(schema.preferencesTable, "piped_instance"); + await m.dropColumn(schema.preferencesTable, "invidious_instance"); + await m.addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ); + await m.dropColumn(schema.sourceMatchTable, "source_id"); + }, ), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 70f6aa26..4a9a7eba 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -558,15 +558,6 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - @override - late final GeneratedColumnWithTypeConverter - audioQuality = GeneratedColumn( - 'audio_quality', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceQualities.high.name)) - .withConverter( - $PreferencesTableTable.$converteraudioQuality); static const VerificationMeta _albumColorSyncMeta = const VerificationMeta('albumColorSync'); @override @@ -703,22 +694,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: const Constant("")) .withConverter>( $PreferencesTableTable.$converterlocalLibraryLocation); - static const VerificationMeta _pipedInstanceMeta = - const VerificationMeta('pipedInstance'); - @override - late final GeneratedColumn pipedInstance = GeneratedColumn( - 'piped_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://pipedapi.kavin.rocks")); - static const VerificationMeta _invidiousInstanceMeta = - const VerificationMeta('invidiousInstance'); - @override - late final GeneratedColumn invidiousInstance = - GeneratedColumn('invidious_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://inv.nadeko.net")); @override late final GeneratedColumnWithTypeConverter themeMode = GeneratedColumn('theme_mode', aliasedName, false, @@ -726,14 +701,12 @@ class $PreferencesTableTable extends PreferencesTable requiredDuringInsert: false, defaultValue: Constant(ThemeMode.system.name)) .withConverter($PreferencesTableTable.$converterthemeMode); + static const VerificationMeta _audioSourceIdMeta = + const VerificationMeta('audioSourceId'); @override - late final GeneratedColumnWithTypeConverter audioSource = - GeneratedColumn('audio_source', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(AudioSource.youtube.name)) - .withConverter( - $PreferencesTableTable.$converteraudioSource); + late final GeneratedColumn audioSourceId = GeneratedColumn( + 'audio_source_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); @override late final GeneratedColumnWithTypeConverter youtubeClientEngine = GeneratedColumn( @@ -743,24 +716,6 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)) .withConverter( $PreferencesTableTable.$converteryoutubeClientEngine); - @override - late final GeneratedColumnWithTypeConverter - streamMusicCodec = GeneratedColumn( - 'stream_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.weba.name)) - .withConverter( - $PreferencesTableTable.$converterstreamMusicCodec); - @override - late final GeneratedColumnWithTypeConverter - downloadMusicCodec = GeneratedColumn( - 'download_music_codec', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceCodecs.m4a.name)) - .withConverter( - $PreferencesTableTable.$converterdownloadMusicCodec); static const VerificationMeta _discordPresenceMeta = const VerificationMeta('discordPresence'); @override @@ -812,7 +767,6 @@ class $PreferencesTableTable extends PreferencesTable @override List get $columns => [ id, - audioQuality, albumColorSync, amoledDarkTheme, checkUpdate, @@ -828,13 +782,9 @@ class $PreferencesTableTable extends PreferencesTable searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, - audioSource, + audioSourceId, youtubeClientEngine, - streamMusicCodec, - downloadMusicCodec, discordPresence, endlessPlayback, enableConnect, @@ -903,17 +853,11 @@ class $PreferencesTableTable extends PreferencesTable downloadLocation.isAcceptableOrUnknown( data['download_location']!, _downloadLocationMeta)); } - if (data.containsKey('piped_instance')) { + if (data.containsKey('audio_source_id')) { context.handle( - _pipedInstanceMeta, - pipedInstance.isAcceptableOrUnknown( - data['piped_instance']!, _pipedInstanceMeta)); - } - if (data.containsKey('invidious_instance')) { - context.handle( - _invidiousInstanceMeta, - invidiousInstance.isAcceptableOrUnknown( - data['invidious_instance']!, _invidiousInstanceMeta)); + _audioSourceIdMeta, + audioSourceId.isAcceptableOrUnknown( + data['audio_source_id']!, _audioSourceIdMeta)); } if (data.containsKey('discord_presence')) { context.handle( @@ -956,9 +900,6 @@ class $PreferencesTableTable extends PreferencesTable return PreferencesTableData( id: attachedDatabase.typeMapping .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), albumColorSync: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, amoledDarkTheme: attachedDatabase.typeMapping.read( @@ -997,25 +938,14 @@ class $PreferencesTableTable extends PreferencesTable .$converterlocalLibraryLocation .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}local_library_location'])!), - pipedInstance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, - invidiousInstance: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), - audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + audioSourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}audio_source_id']), youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}youtube_client_engine'])!), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}stream_music_codec'])!), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}download_music_codec'])!), discordPresence: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, endlessPlayback: attachedDatabase.typeMapping @@ -1034,9 +964,6 @@ class $PreferencesTableTable extends PreferencesTable return $PreferencesTableTable(attachedDatabase, alias); } - static JsonTypeConverter2 - $converteraudioQuality = - const EnumNameConverter(SourceQualities.values); static JsonTypeConverter2 $convertercloseBehavior = const EnumNameConverter(CloseBehavior.values); @@ -1054,23 +981,14 @@ class $PreferencesTableTable extends PreferencesTable const StringListConverter(); static JsonTypeConverter2 $converterthemeMode = const EnumNameConverter(ThemeMode.values); - static JsonTypeConverter2 $converteraudioSource = - const EnumNameConverter(AudioSource.values); static JsonTypeConverter2 $converteryoutubeClientEngine = const EnumNameConverter(YoutubeClientEngine.values); - static JsonTypeConverter2 - $converterstreamMusicCodec = - const EnumNameConverter(SourceCodecs.values); - static JsonTypeConverter2 - $converterdownloadMusicCodec = - const EnumNameConverter(SourceCodecs.values); } class PreferencesTableData extends DataClass implements Insertable { final int id; - final SourceQualities audioQuality; final bool albumColorSync; final bool amoledDarkTheme; final bool checkUpdate; @@ -1086,13 +1004,9 @@ class PreferencesTableData extends DataClass final SearchMode searchMode; final String downloadLocation; final List localLibraryLocation; - final String pipedInstance; - final String invidiousInstance; final ThemeMode themeMode; - final AudioSource audioSource; + final String? audioSourceId; final YoutubeClientEngine youtubeClientEngine; - final SourceCodecs streamMusicCodec; - final SourceCodecs downloadMusicCodec; final bool discordPresence; final bool endlessPlayback; final bool enableConnect; @@ -1100,7 +1014,6 @@ class PreferencesTableData extends DataClass final bool cacheMusic; const PreferencesTableData( {required this.id, - required this.audioQuality, required this.albumColorSync, required this.amoledDarkTheme, required this.checkUpdate, @@ -1116,13 +1029,9 @@ class PreferencesTableData extends DataClass required this.searchMode, required this.downloadLocation, required this.localLibraryLocation, - required this.pipedInstance, - required this.invidiousInstance, required this.themeMode, - required this.audioSource, + this.audioSourceId, required this.youtubeClientEngine, - required this.streamMusicCodec, - required this.downloadMusicCodec, required this.discordPresence, required this.endlessPlayback, required this.enableConnect, @@ -1132,10 +1041,6 @@ class PreferencesTableData extends DataClass Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - { - map['audio_quality'] = Variable( - $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); - } map['album_color_sync'] = Variable(albumColorSync); map['amoled_dark_theme'] = Variable(amoledDarkTheme); map['check_update'] = Variable(checkUpdate); @@ -1174,31 +1079,18 @@ class PreferencesTableData extends DataClass .$converterlocalLibraryLocation .toSql(localLibraryLocation)); } - map['piped_instance'] = Variable(pipedInstance); - map['invidious_instance'] = Variable(invidiousInstance); { map['theme_mode'] = Variable( $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); } - { - map['audio_source'] = Variable( - $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); + if (!nullToAbsent || audioSourceId != null) { + map['audio_source_id'] = Variable(audioSourceId); } { map['youtube_client_engine'] = Variable($PreferencesTableTable .$converteryoutubeClientEngine .toSql(youtubeClientEngine)); } - { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec)); - } - { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec)); - } map['discord_presence'] = Variable(discordPresence); map['endless_playback'] = Variable(endlessPlayback); map['enable_connect'] = Variable(enableConnect); @@ -1210,7 +1102,6 @@ class PreferencesTableData extends DataClass PreferencesTableCompanion toCompanion(bool nullToAbsent) { return PreferencesTableCompanion( id: Value(id), - audioQuality: Value(audioQuality), albumColorSync: Value(albumColorSync), amoledDarkTheme: Value(amoledDarkTheme), checkUpdate: Value(checkUpdate), @@ -1226,13 +1117,11 @@ class PreferencesTableData extends DataClass searchMode: Value(searchMode), downloadLocation: Value(downloadLocation), localLibraryLocation: Value(localLibraryLocation), - pipedInstance: Value(pipedInstance), - invidiousInstance: Value(invidiousInstance), themeMode: Value(themeMode), - audioSource: Value(audioSource), + audioSourceId: audioSourceId == null && nullToAbsent + ? const Value.absent() + : Value(audioSourceId), youtubeClientEngine: Value(youtubeClientEngine), - streamMusicCodec: Value(streamMusicCodec), - downloadMusicCodec: Value(downloadMusicCodec), discordPresence: Value(discordPresence), endlessPlayback: Value(endlessPlayback), enableConnect: Value(enableConnect), @@ -1246,8 +1135,6 @@ class PreferencesTableData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return PreferencesTableData( id: serializer.fromJson(json['id']), - audioQuality: $PreferencesTableTable.$converteraudioQuality - .fromJson(serializer.fromJson(json['audioQuality'])), albumColorSync: serializer.fromJson(json['albumColorSync']), amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), checkUpdate: serializer.fromJson(json['checkUpdate']), @@ -1269,18 +1156,11 @@ class PreferencesTableData extends DataClass downloadLocation: serializer.fromJson(json['downloadLocation']), localLibraryLocation: serializer.fromJson>(json['localLibraryLocation']), - pipedInstance: serializer.fromJson(json['pipedInstance']), - invidiousInstance: serializer.fromJson(json['invidiousInstance']), themeMode: $PreferencesTableTable.$converterthemeMode .fromJson(serializer.fromJson(json['themeMode'])), - audioSource: $PreferencesTableTable.$converteraudioSource - .fromJson(serializer.fromJson(json['audioSource'])), + audioSourceId: serializer.fromJson(json['audioSourceId']), youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine .fromJson(serializer.fromJson(json['youtubeClientEngine'])), - streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec - .fromJson(serializer.fromJson(json['streamMusicCodec'])), - downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec - .fromJson(serializer.fromJson(json['downloadMusicCodec'])), discordPresence: serializer.fromJson(json['discordPresence']), endlessPlayback: serializer.fromJson(json['endlessPlayback']), enableConnect: serializer.fromJson(json['enableConnect']), @@ -1293,8 +1173,6 @@ class PreferencesTableData extends DataClass serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'audioQuality': serializer.toJson( - $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), 'albumColorSync': serializer.toJson(albumColorSync), 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), 'checkUpdate': serializer.toJson(checkUpdate), @@ -1315,21 +1193,12 @@ class PreferencesTableData extends DataClass 'downloadLocation': serializer.toJson(downloadLocation), 'localLibraryLocation': serializer.toJson>(localLibraryLocation), - 'pipedInstance': serializer.toJson(pipedInstance), - 'invidiousInstance': serializer.toJson(invidiousInstance), 'themeMode': serializer.toJson( $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), - 'audioSource': serializer.toJson( - $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'audioSourceId': serializer.toJson(audioSourceId), 'youtubeClientEngine': serializer.toJson($PreferencesTableTable .$converteryoutubeClientEngine .toJson(youtubeClientEngine)), - 'streamMusicCodec': serializer.toJson($PreferencesTableTable - .$converterstreamMusicCodec - .toJson(streamMusicCodec)), - 'downloadMusicCodec': serializer.toJson($PreferencesTableTable - .$converterdownloadMusicCodec - .toJson(downloadMusicCodec)), 'discordPresence': serializer.toJson(discordPresence), 'endlessPlayback': serializer.toJson(endlessPlayback), 'enableConnect': serializer.toJson(enableConnect), @@ -1340,7 +1209,6 @@ class PreferencesTableData extends DataClass PreferencesTableData copyWith( {int? id, - SourceQualities? audioQuality, bool? albumColorSync, bool? amoledDarkTheme, bool? checkUpdate, @@ -1356,13 +1224,9 @@ class PreferencesTableData extends DataClass SearchMode? searchMode, String? downloadLocation, List? localLibraryLocation, - String? pipedInstance, - String? invidiousInstance, ThemeMode? themeMode, - AudioSource? audioSource, + Value audioSourceId = const Value.absent(), YoutubeClientEngine? youtubeClientEngine, - SourceCodecs? streamMusicCodec, - SourceCodecs? downloadMusicCodec, bool? discordPresence, bool? endlessPlayback, bool? enableConnect, @@ -1370,7 +1234,6 @@ class PreferencesTableData extends DataClass bool? cacheMusic}) => PreferencesTableData( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, albumColorSync: albumColorSync ?? this.albumColorSync, amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, checkUpdate: checkUpdate ?? this.checkUpdate, @@ -1386,13 +1249,10 @@ class PreferencesTableData extends DataClass searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, + audioSourceId: + audioSourceId.present ? audioSourceId.value : this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, endlessPlayback: endlessPlayback ?? this.endlessPlayback, enableConnect: enableConnect ?? this.enableConnect, @@ -1402,9 +1262,6 @@ class PreferencesTableData extends DataClass PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) { return PreferencesTableData( id: data.id.present ? data.id.value : this.id, - audioQuality: data.audioQuality.present - ? data.audioQuality.value - : this.audioQuality, albumColorSync: data.albumColorSync.present ? data.albumColorSync.value : this.albumColorSync, @@ -1443,24 +1300,13 @@ class PreferencesTableData extends DataClass localLibraryLocation: data.localLibraryLocation.present ? data.localLibraryLocation.value : this.localLibraryLocation, - pipedInstance: data.pipedInstance.present - ? data.pipedInstance.value - : this.pipedInstance, - invidiousInstance: data.invidiousInstance.present - ? data.invidiousInstance.value - : this.invidiousInstance, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, - audioSource: - data.audioSource.present ? data.audioSource.value : this.audioSource, + audioSourceId: data.audioSourceId.present + ? data.audioSourceId.value + : this.audioSourceId, youtubeClientEngine: data.youtubeClientEngine.present ? data.youtubeClientEngine.value : this.youtubeClientEngine, - streamMusicCodec: data.streamMusicCodec.present - ? data.streamMusicCodec.value - : this.streamMusicCodec, - downloadMusicCodec: data.downloadMusicCodec.present - ? data.downloadMusicCodec.value - : this.downloadMusicCodec, discordPresence: data.discordPresence.present ? data.discordPresence.value : this.discordPresence, @@ -1481,7 +1327,6 @@ class PreferencesTableData extends DataClass String toString() { return (StringBuffer('PreferencesTableData(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') ..write('albumColorSync: $albumColorSync, ') ..write('amoledDarkTheme: $amoledDarkTheme, ') ..write('checkUpdate: $checkUpdate, ') @@ -1497,13 +1342,9 @@ class PreferencesTableData extends DataClass ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') + ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') ..write('endlessPlayback: $endlessPlayback, ') ..write('enableConnect: $enableConnect, ') @@ -1516,7 +1357,6 @@ class PreferencesTableData extends DataClass @override int get hashCode => Object.hashAll([ id, - audioQuality, albumColorSync, amoledDarkTheme, checkUpdate, @@ -1532,13 +1372,9 @@ class PreferencesTableData extends DataClass searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, - audioSource, + audioSourceId, youtubeClientEngine, - streamMusicCodec, - downloadMusicCodec, discordPresence, endlessPlayback, enableConnect, @@ -1550,7 +1386,6 @@ class PreferencesTableData extends DataClass identical(this, other) || (other is PreferencesTableData && other.id == this.id && - other.audioQuality == this.audioQuality && other.albumColorSync == this.albumColorSync && other.amoledDarkTheme == this.amoledDarkTheme && other.checkUpdate == this.checkUpdate && @@ -1566,13 +1401,9 @@ class PreferencesTableData extends DataClass other.searchMode == this.searchMode && other.downloadLocation == this.downloadLocation && other.localLibraryLocation == this.localLibraryLocation && - other.pipedInstance == this.pipedInstance && - other.invidiousInstance == this.invidiousInstance && other.themeMode == this.themeMode && - other.audioSource == this.audioSource && + other.audioSourceId == this.audioSourceId && other.youtubeClientEngine == this.youtubeClientEngine && - other.streamMusicCodec == this.streamMusicCodec && - other.downloadMusicCodec == this.downloadMusicCodec && other.discordPresence == this.discordPresence && other.endlessPlayback == this.endlessPlayback && other.enableConnect == this.enableConnect && @@ -1582,7 +1413,6 @@ class PreferencesTableData extends DataClass class PreferencesTableCompanion extends UpdateCompanion { final Value id; - final Value audioQuality; final Value albumColorSync; final Value amoledDarkTheme; final Value checkUpdate; @@ -1598,13 +1428,9 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value searchMode; final Value downloadLocation; final Value> localLibraryLocation; - final Value pipedInstance; - final Value invidiousInstance; final Value themeMode; - final Value audioSource; + final Value audioSourceId; final Value youtubeClientEngine; - final Value streamMusicCodec; - final Value downloadMusicCodec; final Value discordPresence; final Value endlessPlayback; final Value enableConnect; @@ -1612,7 +1438,6 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value cacheMusic; const PreferencesTableCompanion({ this.id = const Value.absent(), - this.audioQuality = const Value.absent(), this.albumColorSync = const Value.absent(), this.amoledDarkTheme = const Value.absent(), this.checkUpdate = const Value.absent(), @@ -1628,13 +1453,9 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), + this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), this.endlessPlayback = const Value.absent(), this.enableConnect = const Value.absent(), @@ -1643,7 +1464,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }); PreferencesTableCompanion.insert({ this.id = const Value.absent(), - this.audioQuality = const Value.absent(), this.albumColorSync = const Value.absent(), this.amoledDarkTheme = const Value.absent(), this.checkUpdate = const Value.absent(), @@ -1659,13 +1479,9 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), - this.audioSource = const Value.absent(), + this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), - this.streamMusicCodec = const Value.absent(), - this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), this.endlessPlayback = const Value.absent(), this.enableConnect = const Value.absent(), @@ -1674,7 +1490,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }); static Insertable custom({ Expression? id, - Expression? audioQuality, Expression? albumColorSync, Expression? amoledDarkTheme, Expression? checkUpdate, @@ -1690,13 +1505,9 @@ class PreferencesTableCompanion extends UpdateCompanion { Expression? searchMode, Expression? downloadLocation, Expression? localLibraryLocation, - Expression? pipedInstance, - Expression? invidiousInstance, Expression? themeMode, - Expression? audioSource, + Expression? audioSourceId, Expression? youtubeClientEngine, - Expression? streamMusicCodec, - Expression? downloadMusicCodec, Expression? discordPresence, Expression? endlessPlayback, Expression? enableConnect, @@ -1705,7 +1516,6 @@ class PreferencesTableCompanion extends UpdateCompanion { }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (audioQuality != null) 'audio_quality': audioQuality, if (albumColorSync != null) 'album_color_sync': albumColorSync, if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, if (checkUpdate != null) 'check_update': checkUpdate, @@ -1723,15 +1533,10 @@ class PreferencesTableCompanion extends UpdateCompanion { if (downloadLocation != null) 'download_location': downloadLocation, if (localLibraryLocation != null) 'local_library_location': localLibraryLocation, - if (pipedInstance != null) 'piped_instance': pipedInstance, - if (invidiousInstance != null) 'invidious_instance': invidiousInstance, if (themeMode != null) 'theme_mode': themeMode, - if (audioSource != null) 'audio_source': audioSource, + if (audioSourceId != null) 'audio_source_id': audioSourceId, if (youtubeClientEngine != null) 'youtube_client_engine': youtubeClientEngine, - if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, - if (downloadMusicCodec != null) - 'download_music_codec': downloadMusicCodec, if (discordPresence != null) 'discord_presence': discordPresence, if (endlessPlayback != null) 'endless_playback': endlessPlayback, if (enableConnect != null) 'enable_connect': enableConnect, @@ -1742,7 +1547,6 @@ class PreferencesTableCompanion extends UpdateCompanion { PreferencesTableCompanion copyWith( {Value? id, - Value? audioQuality, Value? albumColorSync, Value? amoledDarkTheme, Value? checkUpdate, @@ -1758,13 +1562,9 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? searchMode, Value? downloadLocation, Value>? localLibraryLocation, - Value? pipedInstance, - Value? invidiousInstance, Value? themeMode, - Value? audioSource, + Value? audioSourceId, Value? youtubeClientEngine, - Value? streamMusicCodec, - Value? downloadMusicCodec, Value? discordPresence, Value? endlessPlayback, Value? enableConnect, @@ -1772,7 +1572,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? cacheMusic}) { return PreferencesTableCompanion( id: id ?? this.id, - audioQuality: audioQuality ?? this.audioQuality, albumColorSync: albumColorSync ?? this.albumColorSync, amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, checkUpdate: checkUpdate ?? this.checkUpdate, @@ -1788,13 +1587,9 @@ class PreferencesTableCompanion extends UpdateCompanion { searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, - audioSource: audioSource ?? this.audioSource, + audioSourceId: audioSourceId ?? this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, - streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, - downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, endlessPlayback: endlessPlayback ?? this.endlessPlayback, enableConnect: enableConnect ?? this.enableConnect, @@ -1809,11 +1604,6 @@ class PreferencesTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (audioQuality.present) { - map['audio_quality'] = Variable($PreferencesTableTable - .$converteraudioQuality - .toSql(audioQuality.value)); - } if (albumColorSync.present) { map['album_color_sync'] = Variable(albumColorSync.value); } @@ -1869,36 +1659,18 @@ class PreferencesTableCompanion extends UpdateCompanion { .$converterlocalLibraryLocation .toSql(localLibraryLocation.value)); } - if (pipedInstance.present) { - map['piped_instance'] = Variable(pipedInstance.value); - } - if (invidiousInstance.present) { - map['invidious_instance'] = Variable(invidiousInstance.value); - } if (themeMode.present) { map['theme_mode'] = Variable( $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); } - if (audioSource.present) { - map['audio_source'] = Variable($PreferencesTableTable - .$converteraudioSource - .toSql(audioSource.value)); + if (audioSourceId.present) { + map['audio_source_id'] = Variable(audioSourceId.value); } if (youtubeClientEngine.present) { map['youtube_client_engine'] = Variable($PreferencesTableTable .$converteryoutubeClientEngine .toSql(youtubeClientEngine.value)); } - if (streamMusicCodec.present) { - map['stream_music_codec'] = Variable($PreferencesTableTable - .$converterstreamMusicCodec - .toSql(streamMusicCodec.value)); - } - if (downloadMusicCodec.present) { - map['download_music_codec'] = Variable($PreferencesTableTable - .$converterdownloadMusicCodec - .toSql(downloadMusicCodec.value)); - } if (discordPresence.present) { map['discord_presence'] = Variable(discordPresence.value); } @@ -1921,7 +1693,6 @@ class PreferencesTableCompanion extends UpdateCompanion { String toString() { return (StringBuffer('PreferencesTableCompanion(') ..write('id: $id, ') - ..write('audioQuality: $audioQuality, ') ..write('albumColorSync: $albumColorSync, ') ..write('amoledDarkTheme: $amoledDarkTheme, ') ..write('checkUpdate: $checkUpdate, ') @@ -1937,13 +1708,9 @@ class PreferencesTableCompanion extends UpdateCompanion { ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') - ..write('audioSource: $audioSource, ') + ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') - ..write('streamMusicCodec: $streamMusicCodec, ') - ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') ..write('endlessPlayback: $endlessPlayback, ') ..write('enableConnect: $enableConnect, ') @@ -2539,20 +2306,20 @@ class $SourceMatchTableTable extends SourceMatchTable late final GeneratedColumn trackId = GeneratedColumn( 'track_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _sourceIdMeta = - const VerificationMeta('sourceId'); + static const VerificationMeta _sourceInfoMeta = + const VerificationMeta('sourceInfo'); @override - late final GeneratedColumn sourceId = GeneratedColumn( - 'source_id', aliasedName, false, + late final GeneratedColumn sourceInfo = GeneratedColumn( + 'source_info', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("{}")); + static const VerificationMeta _sourceTypeMeta = + const VerificationMeta('sourceType'); + @override + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - @override - late final GeneratedColumnWithTypeConverter sourceType = - GeneratedColumn('source_type', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: Constant(SourceType.youtube.name)) - .withConverter( - $SourceMatchTableTable.$convertersourceType); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -2563,7 +2330,7 @@ class $SourceMatchTableTable extends SourceMatchTable defaultValue: currentDateAndTime); @override List get $columns => - [id, trackId, sourceId, sourceType, createdAt]; + [id, trackId, sourceInfo, sourceType, createdAt]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2584,11 +2351,19 @@ class $SourceMatchTableTable extends SourceMatchTable } else if (isInserting) { context.missing(_trackIdMeta); } - if (data.containsKey('source_id')) { - context.handle(_sourceIdMeta, - sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); + if (data.containsKey('source_info')) { + context.handle( + _sourceInfoMeta, + sourceInfo.isAcceptableOrUnknown( + data['source_info']!, _sourceInfoMeta)); + } + if (data.containsKey('source_type')) { + context.handle( + _sourceTypeMeta, + sourceType.isAcceptableOrUnknown( + data['source_type']!, _sourceTypeMeta)); } else if (isInserting) { - context.missing(_sourceIdMeta); + context.missing(_sourceTypeMeta); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, @@ -2607,11 +2382,10 @@ class $SourceMatchTableTable extends SourceMatchTable .read(DriftSqlType.int, data['${effectivePrefix}id'])!, trackId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, - sourceId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, - sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}source_type'])!), + sourceInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_info'])!, + sourceType: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_type'])!, createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, ); @@ -2621,22 +2395,19 @@ class $SourceMatchTableTable extends SourceMatchTable $SourceMatchTableTable createAlias(String alias) { return $SourceMatchTableTable(attachedDatabase, alias); } - - static JsonTypeConverter2 $convertersourceType = - const EnumNameConverter(SourceType.values); } class SourceMatchTableData extends DataClass implements Insertable { final int id; final String trackId; - final String sourceId; - final SourceType sourceType; + final String sourceInfo; + final String sourceType; final DateTime createdAt; const SourceMatchTableData( {required this.id, required this.trackId, - required this.sourceId, + required this.sourceInfo, required this.sourceType, required this.createdAt}); @override @@ -2644,11 +2415,8 @@ class SourceMatchTableData extends DataClass final map = {}; map['id'] = Variable(id); map['track_id'] = Variable(trackId); - map['source_id'] = Variable(sourceId); - { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); - } + map['source_info'] = Variable(sourceInfo); + map['source_type'] = Variable(sourceType); map['created_at'] = Variable(createdAt); return map; } @@ -2657,7 +2425,7 @@ class SourceMatchTableData extends DataClass return SourceMatchTableCompanion( id: Value(id), trackId: Value(trackId), - sourceId: Value(sourceId), + sourceInfo: Value(sourceInfo), sourceType: Value(sourceType), createdAt: Value(createdAt), ); @@ -2669,9 +2437,8 @@ class SourceMatchTableData extends DataClass return SourceMatchTableData( id: serializer.fromJson(json['id']), trackId: serializer.fromJson(json['trackId']), - sourceId: serializer.fromJson(json['sourceId']), - sourceType: $SourceMatchTableTable.$convertersourceType - .fromJson(serializer.fromJson(json['sourceType'])), + sourceInfo: serializer.fromJson(json['sourceInfo']), + sourceType: serializer.fromJson(json['sourceType']), createdAt: serializer.fromJson(json['createdAt']), ); } @@ -2681,9 +2448,8 @@ class SourceMatchTableData extends DataClass return { 'id': serializer.toJson(id), 'trackId': serializer.toJson(trackId), - 'sourceId': serializer.toJson(sourceId), - 'sourceType': serializer.toJson( - $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), + 'sourceInfo': serializer.toJson(sourceInfo), + 'sourceType': serializer.toJson(sourceType), 'createdAt': serializer.toJson(createdAt), }; } @@ -2691,13 +2457,13 @@ class SourceMatchTableData extends DataClass SourceMatchTableData copyWith( {int? id, String? trackId, - String? sourceId, - SourceType? sourceType, + String? sourceInfo, + String? sourceType, DateTime? createdAt}) => SourceMatchTableData( id: id ?? this.id, trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, + sourceInfo: sourceInfo ?? this.sourceInfo, sourceType: sourceType ?? this.sourceType, createdAt: createdAt ?? this.createdAt, ); @@ -2705,7 +2471,8 @@ class SourceMatchTableData extends DataClass return SourceMatchTableData( id: data.id.present ? data.id.value : this.id, trackId: data.trackId.present ? data.trackId.value : this.trackId, - sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + sourceInfo: + data.sourceInfo.present ? data.sourceInfo.value : this.sourceInfo, sourceType: data.sourceType.present ? data.sourceType.value : this.sourceType, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -2717,7 +2484,7 @@ class SourceMatchTableData extends DataClass return (StringBuffer('SourceMatchTableData(') ..write('id: $id, ') ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') + ..write('sourceInfo: $sourceInfo, ') ..write('sourceType: $sourceType, ') ..write('createdAt: $createdAt') ..write(')')) @@ -2725,14 +2492,15 @@ class SourceMatchTableData extends DataClass } @override - int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + int get hashCode => + Object.hash(id, trackId, sourceInfo, sourceType, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is SourceMatchTableData && other.id == this.id && other.trackId == this.trackId && - other.sourceId == this.sourceId && + other.sourceInfo == this.sourceInfo && other.sourceType == this.sourceType && other.createdAt == this.createdAt); } @@ -2740,35 +2508,35 @@ class SourceMatchTableData extends DataClass class SourceMatchTableCompanion extends UpdateCompanion { final Value id; final Value trackId; - final Value sourceId; - final Value sourceType; + final Value sourceInfo; + final Value sourceType; final Value createdAt; const SourceMatchTableCompanion({ this.id = const Value.absent(), this.trackId = const Value.absent(), - this.sourceId = const Value.absent(), + this.sourceInfo = const Value.absent(), this.sourceType = const Value.absent(), this.createdAt = const Value.absent(), }); SourceMatchTableCompanion.insert({ this.id = const Value.absent(), required String trackId, - required String sourceId, - this.sourceType = const Value.absent(), + this.sourceInfo = const Value.absent(), + required String sourceType, this.createdAt = const Value.absent(), }) : trackId = Value(trackId), - sourceId = Value(sourceId); + sourceType = Value(sourceType); static Insertable custom({ Expression? id, Expression? trackId, - Expression? sourceId, + Expression? sourceInfo, Expression? sourceType, Expression? createdAt, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (trackId != null) 'track_id': trackId, - if (sourceId != null) 'source_id': sourceId, + if (sourceInfo != null) 'source_info': sourceInfo, if (sourceType != null) 'source_type': sourceType, if (createdAt != null) 'created_at': createdAt, }); @@ -2777,13 +2545,13 @@ class SourceMatchTableCompanion extends UpdateCompanion { SourceMatchTableCompanion copyWith( {Value? id, Value? trackId, - Value? sourceId, - Value? sourceType, + Value? sourceInfo, + Value? sourceType, Value? createdAt}) { return SourceMatchTableCompanion( id: id ?? this.id, trackId: trackId ?? this.trackId, - sourceId: sourceId ?? this.sourceId, + sourceInfo: sourceInfo ?? this.sourceInfo, sourceType: sourceType ?? this.sourceType, createdAt: createdAt ?? this.createdAt, ); @@ -2798,12 +2566,11 @@ class SourceMatchTableCompanion extends UpdateCompanion { if (trackId.present) { map['track_id'] = Variable(trackId.value); } - if (sourceId.present) { - map['source_id'] = Variable(sourceId.value); + if (sourceInfo.present) { + map['source_info'] = Variable(sourceInfo.value); } if (sourceType.present) { - map['source_type'] = Variable( - $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + map['source_type'] = Variable(sourceType.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -2816,7 +2583,7 @@ class SourceMatchTableCompanion extends UpdateCompanion { return (StringBuffer('SourceMatchTableCompanion(') ..write('id: $id, ') ..write('trackId: $trackId, ') - ..write('sourceId: $sourceId, ') + ..write('sourceInfo: $sourceInfo, ') ..write('sourceType: $sourceType, ') ..write('createdAt: $createdAt') ..write(')')) @@ -4377,7 +4144,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); late final Index uniqTrackMatch = Index('uniq_track_match', - 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4719,7 +4486,6 @@ typedef $$BlacklistTableTableProcessedTableManager = ProcessedTableManager< typedef $$PreferencesTableTableCreateCompanionBuilder = PreferencesTableCompanion Function({ Value id, - Value audioQuality, Value albumColorSync, Value amoledDarkTheme, Value checkUpdate, @@ -4735,13 +4501,9 @@ typedef $$PreferencesTableTableCreateCompanionBuilder Value searchMode, Value downloadLocation, Value> localLibraryLocation, - Value pipedInstance, - Value invidiousInstance, Value themeMode, - Value audioSource, + Value audioSourceId, Value youtubeClientEngine, - Value streamMusicCodec, - Value downloadMusicCodec, Value discordPresence, Value endlessPlayback, Value enableConnect, @@ -4751,7 +4513,6 @@ typedef $$PreferencesTableTableCreateCompanionBuilder typedef $$PreferencesTableTableUpdateCompanionBuilder = PreferencesTableCompanion Function({ Value id, - Value audioQuality, Value albumColorSync, Value amoledDarkTheme, Value checkUpdate, @@ -4767,13 +4528,9 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder Value searchMode, Value downloadLocation, Value> localLibraryLocation, - Value pipedInstance, - Value invidiousInstance, Value themeMode, - Value audioSource, + Value audioSourceId, Value youtubeClientEngine, - Value streamMusicCodec, - Value downloadMusicCodec, Value discordPresence, Value endlessPlayback, Value enableConnect, @@ -4793,11 +4550,6 @@ class $$PreferencesTableTableFilterComposer ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get audioQuality => $composableBuilder( - column: $table.audioQuality, - builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => ColumnFilters(column)); @@ -4863,22 +4615,13 @@ class $$PreferencesTableTableFilterComposer column: $table.localLibraryLocation, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get pipedInstance => $composableBuilder( - column: $table.pipedInstance, builder: (column) => ColumnFilters(column)); - - ColumnFilters get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, - builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters get themeMode => $composableBuilder( column: $table.themeMode, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters - get audioSource => $composableBuilder( - column: $table.audioSource, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get audioSourceId => $composableBuilder( + column: $table.audioSourceId, builder: (column) => ColumnFilters(column)); ColumnWithTypeConverterFilters @@ -4886,16 +4629,6 @@ class $$PreferencesTableTableFilterComposer column: $table.youtubeClientEngine, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters - get streamMusicCodec => $composableBuilder( - column: $table.streamMusicCodec, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnWithTypeConverterFilters - get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, - builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => ColumnFilters(column)); @@ -4926,10 +4659,6 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get audioQuality => $composableBuilder( - column: $table.audioQuality, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => ColumnOrderings(column)); @@ -4985,32 +4714,17 @@ class $$PreferencesTableTableOrderingComposer column: $table.localLibraryLocation, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pipedInstance => $composableBuilder( - column: $table.pipedInstance, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get themeMode => $composableBuilder( column: $table.themeMode, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get audioSource => $composableBuilder( - column: $table.audioSource, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get audioSourceId => $composableBuilder( + column: $table.audioSourceId, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get youtubeClientEngine => $composableBuilder( column: $table.youtubeClientEngine, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get streamMusicCodec => $composableBuilder( - column: $table.streamMusicCodec, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => ColumnOrderings(column)); @@ -5042,10 +4756,6 @@ class $$PreferencesTableTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumnWithTypeConverter get audioQuality => - $composableBuilder( - column: $table.audioQuality, builder: (column) => column); - GeneratedColumn get albumColorSync => $composableBuilder( column: $table.albumColorSync, builder: (column) => column); @@ -5096,31 +4806,16 @@ class $$PreferencesTableTableAnnotationComposer get localLibraryLocation => $composableBuilder( column: $table.localLibraryLocation, builder: (column) => column); - GeneratedColumn get pipedInstance => $composableBuilder( - column: $table.pipedInstance, builder: (column) => column); - - GeneratedColumn get invidiousInstance => $composableBuilder( - column: $table.invidiousInstance, builder: (column) => column); - GeneratedColumnWithTypeConverter get themeMode => $composableBuilder(column: $table.themeMode, builder: (column) => column); - GeneratedColumnWithTypeConverter get audioSource => - $composableBuilder( - column: $table.audioSource, builder: (column) => column); + GeneratedColumn get audioSourceId => $composableBuilder( + column: $table.audioSourceId, builder: (column) => column); GeneratedColumnWithTypeConverter get youtubeClientEngine => $composableBuilder( column: $table.youtubeClientEngine, builder: (column) => column); - GeneratedColumnWithTypeConverter get streamMusicCodec => - $composableBuilder( - column: $table.streamMusicCodec, builder: (column) => column); - - GeneratedColumnWithTypeConverter - get downloadMusicCodec => $composableBuilder( - column: $table.downloadMusicCodec, builder: (column) => column); - GeneratedColumn get discordPresence => $composableBuilder( column: $table.discordPresence, builder: (column) => column); @@ -5166,7 +4861,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< $$PreferencesTableTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), - Value audioQuality = const Value.absent(), Value albumColorSync = const Value.absent(), Value amoledDarkTheme = const Value.absent(), Value checkUpdate = const Value.absent(), @@ -5182,14 +4876,10 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value searchMode = const Value.absent(), Value downloadLocation = const Value.absent(), Value> localLibraryLocation = const Value.absent(), - Value pipedInstance = const Value.absent(), - Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), - Value audioSource = const Value.absent(), + Value audioSourceId = const Value.absent(), Value youtubeClientEngine = const Value.absent(), - Value streamMusicCodec = const Value.absent(), - Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), Value endlessPlayback = const Value.absent(), Value enableConnect = const Value.absent(), @@ -5198,7 +4888,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< }) => PreferencesTableCompanion( id: id, - audioQuality: audioQuality, albumColorSync: albumColorSync, amoledDarkTheme: amoledDarkTheme, checkUpdate: checkUpdate, @@ -5214,13 +4903,9 @@ class $$PreferencesTableTableTableManager extends RootTableManager< searchMode: searchMode, downloadLocation: downloadLocation, localLibraryLocation: localLibraryLocation, - pipedInstance: pipedInstance, - invidiousInstance: invidiousInstance, themeMode: themeMode, - audioSource: audioSource, + audioSourceId: audioSourceId, youtubeClientEngine: youtubeClientEngine, - streamMusicCodec: streamMusicCodec, - downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, endlessPlayback: endlessPlayback, enableConnect: enableConnect, @@ -5229,7 +4914,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< ), createCompanionCallback: ({ Value id = const Value.absent(), - Value audioQuality = const Value.absent(), Value albumColorSync = const Value.absent(), Value amoledDarkTheme = const Value.absent(), Value checkUpdate = const Value.absent(), @@ -5245,14 +4929,10 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value searchMode = const Value.absent(), Value downloadLocation = const Value.absent(), Value> localLibraryLocation = const Value.absent(), - Value pipedInstance = const Value.absent(), - Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), - Value audioSource = const Value.absent(), + Value audioSourceId = const Value.absent(), Value youtubeClientEngine = const Value.absent(), - Value streamMusicCodec = const Value.absent(), - Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), Value endlessPlayback = const Value.absent(), Value enableConnect = const Value.absent(), @@ -5261,7 +4941,6 @@ class $$PreferencesTableTableTableManager extends RootTableManager< }) => PreferencesTableCompanion.insert( id: id, - audioQuality: audioQuality, albumColorSync: albumColorSync, amoledDarkTheme: amoledDarkTheme, checkUpdate: checkUpdate, @@ -5277,13 +4956,9 @@ class $$PreferencesTableTableTableManager extends RootTableManager< searchMode: searchMode, downloadLocation: downloadLocation, localLibraryLocation: localLibraryLocation, - pipedInstance: pipedInstance, - invidiousInstance: invidiousInstance, themeMode: themeMode, - audioSource: audioSource, + audioSourceId: audioSourceId, youtubeClientEngine: youtubeClientEngine, - streamMusicCodec: streamMusicCodec, - downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, endlessPlayback: endlessPlayback, enableConnect: enableConnect, @@ -5644,16 +5319,16 @@ typedef $$SourceMatchTableTableCreateCompanionBuilder = SourceMatchTableCompanion Function({ Value id, required String trackId, - required String sourceId, - Value sourceType, + Value sourceInfo, + required String sourceType, Value createdAt, }); typedef $$SourceMatchTableTableUpdateCompanionBuilder = SourceMatchTableCompanion Function({ Value id, Value trackId, - Value sourceId, - Value sourceType, + Value sourceInfo, + Value sourceType, Value createdAt, }); @@ -5672,13 +5347,11 @@ class $$SourceMatchTableTableFilterComposer ColumnFilters get trackId => $composableBuilder( column: $table.trackId, builder: (column) => ColumnFilters(column)); - ColumnFilters get sourceId => $composableBuilder( - column: $table.sourceId, builder: (column) => ColumnFilters(column)); + ColumnFilters get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters - get sourceType => $composableBuilder( - column: $table.sourceType, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); @@ -5699,8 +5372,8 @@ class $$SourceMatchTableTableOrderingComposer ColumnOrderings get trackId => $composableBuilder( column: $table.trackId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get sourceId => $composableBuilder( - column: $table.sourceId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => ColumnOrderings(column)); ColumnOrderings get sourceType => $composableBuilder( column: $table.sourceType, builder: (column) => ColumnOrderings(column)); @@ -5724,12 +5397,11 @@ class $$SourceMatchTableTableAnnotationComposer GeneratedColumn get trackId => $composableBuilder(column: $table.trackId, builder: (column) => column); - GeneratedColumn get sourceId => - $composableBuilder(column: $table.sourceId, builder: (column) => column); + GeneratedColumn get sourceInfo => $composableBuilder( + column: $table.sourceInfo, builder: (column) => column); - GeneratedColumnWithTypeConverter get sourceType => - $composableBuilder( - column: $table.sourceType, builder: (column) => column); + GeneratedColumn get sourceType => $composableBuilder( + column: $table.sourceType, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -5765,28 +5437,28 @@ class $$SourceMatchTableTableTableManager extends RootTableManager< updateCompanionCallback: ({ Value id = const Value.absent(), Value trackId = const Value.absent(), - Value sourceId = const Value.absent(), - Value sourceType = const Value.absent(), + Value sourceInfo = const Value.absent(), + Value sourceType = const Value.absent(), Value createdAt = const Value.absent(), }) => SourceMatchTableCompanion( id: id, trackId: trackId, - sourceId: sourceId, + sourceInfo: sourceInfo, sourceType: sourceType, createdAt: createdAt, ), createCompanionCallback: ({ Value id = const Value.absent(), required String trackId, - required String sourceId, - Value sourceType = const Value.absent(), + Value sourceInfo = const Value.absent(), + required String sourceType, Value createdAt = const Value.absent(), }) => SourceMatchTableCompanion.insert( id: id, trackId: trackId, - sourceId: sourceId, + sourceInfo: sourceInfo, sourceType: sourceType, createdAt: createdAt, ), diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index babe71b9..42cbdf6d 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -5,7 +5,6 @@ import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import import 'package:flutter/material.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/market.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; // GENERATED BY drift_dev, DO NOT MODIFY. final class Schema2 extends i0.VersionedSchema { @@ -330,8 +329,7 @@ class Shape2 extends i0.VersionedTable { i1.GeneratedColumn _column_7(String aliasedName) => i1.GeneratedColumn('audio_quality', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceQualities.high.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("high")); i1.GeneratedColumn _column_8(String aliasedName) => i1.GeneratedColumn('album_color_sync', aliasedName, false, type: i1.DriftSqlType.bool, @@ -418,16 +416,13 @@ i1.GeneratedColumn _column_25(String aliasedName) => defaultValue: Constant(ThemeMode.system.name)); i1.GeneratedColumn _column_26(String aliasedName) => i1.GeneratedColumn('audio_source', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(AudioSource.youtube.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("youtube")); i1.GeneratedColumn _column_27(String aliasedName) => i1.GeneratedColumn('stream_music_codec', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceCodecs.weba.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("weba")); i1.GeneratedColumn _column_28(String aliasedName) => i1.GeneratedColumn('download_music_codec', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceCodecs.m4a.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("m4a")); i1.GeneratedColumn _column_29(String aliasedName) => i1.GeneratedColumn('discord_presence', aliasedName, false, type: i1.DriftSqlType.bool, @@ -512,8 +507,7 @@ i1.GeneratedColumn _column_38(String aliasedName) => type: i1.DriftSqlType.string); i1.GeneratedColumn _column_39(String aliasedName) => i1.GeneratedColumn('source_type', aliasedName, false, - type: i1.DriftSqlType.string, - defaultValue: Constant(SourceType.youtube.name)); + type: i1.DriftSqlType.string, defaultValue: Constant("youtube")); class Shape6 extends i0.VersionedTable { Shape6({required super.source, required super.alias}) : super.aliased(); @@ -2462,6 +2456,289 @@ i1.GeneratedColumn _column_72(String aliasedName) => i1.GeneratedColumn _column_73(String aliasedName) => i1.GeneratedColumn('plugin_api_version', aliasedName, false, type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0')); + +final class Schema10 extends i0.VersionedSchema { + Schema10({required super.database}) : super(version: 10); + @override + late final List entities = [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + historyTable, + lyricsTable, + pluginsTable, + uniqueBlacklist, + uniqTrackMatch, + ]; + late final Shape0 authenticationTable = Shape0( + source: i0.VersionedTable( + entityName: 'authentication_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 blacklistTable = Shape1( + source: i0.VersionedTable( + entityName: 'blacklist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_4, + _column_5, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape17 preferencesTable = Shape17( + source: i0.VersionedTable( + entityName: 'preferences_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + _column_69, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_25, + _column_74, + _column_54, + _column_29, + _column_30, + _column_31, + _column_56, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 scrobblerTable = Shape3( + source: i0.VersionedTable( + entityName: 'scrobbler_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_33, + _column_34, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 skipSegmentTable = Shape4( + source: i0.VersionedTable( + entityName: 'skip_segment_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_35, + _column_36, + _column_37, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape18 sourceMatchTable = Shape18( + source: i0.VersionedTable( + entityName: 'source_match_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_75, + _column_76, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape14 audioPlayerStateTable = Shape14( + source: i0.VersionedTable( + entityName: 'audio_player_state_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_40, + _column_41, + _column_42, + _column_43, + _column_57, + _column_58, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 historyTable = Shape9( + source: i0.VersionedTable( + entityName: 'history_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_50, + _column_51, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 lyricsTable = Shape10( + source: i0.VersionedTable( + entityName: 'lyrics_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape16 pluginsTable = Shape16( + source: i0.VersionedTable( + entityName: 'plugins_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_59, + _column_60, + _column_61, + _column_62, + _column_63, + _column_64, + _column_65, + _column_71, + _column_72, + _column_67, + _column_73, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index uniqueBlacklist = i1.Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + final i1.Index uniqTrackMatch = i1.Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); +} + +class Shape17 extends i0.VersionedTable { + Shape17({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumColorSync => + columnsByName['album_color_sync']! as i1.GeneratedColumn; + i1.GeneratedColumn get amoledDarkTheme => + columnsByName['amoled_dark_theme']! as i1.GeneratedColumn; + i1.GeneratedColumn get checkUpdate => + columnsByName['check_update']! as i1.GeneratedColumn; + i1.GeneratedColumn get normalizeAudio => + columnsByName['normalize_audio']! as i1.GeneratedColumn; + i1.GeneratedColumn get showSystemTrayIcon => + columnsByName['show_system_tray_icon']! as i1.GeneratedColumn; + i1.GeneratedColumn get systemTitleBar => + columnsByName['system_title_bar']! as i1.GeneratedColumn; + i1.GeneratedColumn get skipNonMusic => + columnsByName['skip_non_music']! as i1.GeneratedColumn; + i1.GeneratedColumn get closeBehavior => + columnsByName['close_behavior']! as i1.GeneratedColumn; + i1.GeneratedColumn get accentColorScheme => + columnsByName['accent_color_scheme']! as i1.GeneratedColumn; + i1.GeneratedColumn get layoutMode => + columnsByName['layout_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get locale => + columnsByName['locale']! as i1.GeneratedColumn; + i1.GeneratedColumn get market => + columnsByName['market']! as i1.GeneratedColumn; + i1.GeneratedColumn get searchMode => + columnsByName['search_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadLocation => + columnsByName['download_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get localLibraryLocation => + columnsByName['local_library_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get themeMode => + columnsByName['theme_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get audioSourceId => + columnsByName['audio_source_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get youtubeClientEngine => + columnsByName['youtube_client_engine']! as i1.GeneratedColumn; + i1.GeneratedColumn get discordPresence => + columnsByName['discord_presence']! as i1.GeneratedColumn; + i1.GeneratedColumn get endlessPlayback => + columnsByName['endless_playback']! as i1.GeneratedColumn; + i1.GeneratedColumn get enableConnect => + columnsByName['enable_connect']! as i1.GeneratedColumn; + i1.GeneratedColumn get connectPort => + columnsByName['connect_port']! as i1.GeneratedColumn; + i1.GeneratedColumn get cacheMusic => + columnsByName['cache_music']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_74(String aliasedName) => + i1.GeneratedColumn('audio_source_id', aliasedName, true, + type: i1.DriftSqlType.string); + +class Shape18 extends i0.VersionedTable { + Shape18({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get trackId => + columnsByName['track_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceInfo => + columnsByName['source_info']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceType => + columnsByName['source_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_75(String aliasedName) => + i1.GeneratedColumn('source_info', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const Constant("{}")); +i1.GeneratedColumn _column_76(String aliasedName) => + i1.GeneratedColumn('source_type', aliasedName, false, + type: i1.DriftSqlType.string); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -2471,6 +2748,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -2514,6 +2792,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from8To9(migrator, schema); return 9; + case 9: + final schema = Schema10(database: database); + final migrator = i1.Migrator(database, schema); + await from9To10(migrator, schema); + return 10; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -2529,6 +2812,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -2540,4 +2824,5 @@ i1.OnUpgrade stepByStep({ from6To7: from6To7, from7To8: from7To8, from8To9: from8To9, + from9To10: from9To10, )); diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index ea2f7538..cc810ae7 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -76,10 +76,6 @@ class PreferencesTable extends Table { TextColumn get downloadLocation => text().withDefault(const Constant(""))(); TextColumn get localLibraryLocation => text().withDefault(const Constant("")).map(const StringListConverter())(); - TextColumn get pipedInstance => - text().withDefault(const Constant("https://pipedapi.kavin.rocks"))(); - TextColumn get invidiousInstance => - text().withDefault(const Constant("https://inv.nadeko.net"))(); TextColumn get themeMode => textEnum().withDefault(Constant(ThemeMode.system.name))(); TextColumn get audioSourceId => text().nullable()(); @@ -113,8 +109,6 @@ class PreferencesTable extends Table { searchMode: SearchMode.youtube, downloadLocation: "", localLibraryLocation: [], - pipedInstance: "https://pipedapi.kavin.rocks", - invidiousInstance: "https://inv.nadeko.net", themeMode: ThemeMode.system, audioSourceId: null, youtubeClientEngine: YoutubeClientEngine.youtubeExplode, diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index b5661137..9ef79e9b 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -8,7 +8,7 @@ part of '../database.dart'; class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); - TextColumn get sourceInfo => text()(); + TextColumn get sourceInfo => text().withDefault(const Constant("{}"))(); TextColumn get sourceType => text()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart index c429ec74..44804285 100644 --- a/lib/models/metadata/audio_source.dart +++ b/lib/models/metadata/audio_source.dart @@ -1,5 +1,7 @@ part of 'metadata.dart'; +final oneOptionalDecimalFormatter = NumberFormat('0.#', 'en_US'); + enum SpotubeMediaCompressionType { lossy, lossless, @@ -30,26 +32,40 @@ class SpotubeAudioSourceContainerPreset @freezed class SpotubeAudioLossyContainerQuality with _$SpotubeAudioLossyContainerQuality { + const SpotubeAudioLossyContainerQuality._(); + factory SpotubeAudioLossyContainerQuality({ - required double bitrate, + required int bitrate, // bits per second }) = _SpotubeAudioLossyContainerQuality; factory SpotubeAudioLossyContainerQuality.fromJson( Map json) => _$SpotubeAudioLossyContainerQualityFromJson(json); + + @override + toString() { + return "${oneOptionalDecimalFormatter.format(bitrate)}kbps"; + } } @freezed class SpotubeAudioLosslessContainerQuality with _$SpotubeAudioLosslessContainerQuality { + const SpotubeAudioLosslessContainerQuality._(); + factory SpotubeAudioLosslessContainerQuality({ - required int bitDepth, - required double sampleRate, + required int bitDepth, // bit + required int sampleRate, // hz }) = _SpotubeAudioLosslessContainerQuality; factory SpotubeAudioLosslessContainerQuality.fromJson( Map json) => _$SpotubeAudioLosslessContainerQualityFromJson(json); + + @override + toString() { + return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz"; + } } @freezed diff --git a/lib/models/metadata/metadata.dart b/lib/models/metadata/metadata.dart index 4c6eb2ac..e68bcd14 100644 --- a/lib/models/metadata/metadata.dart +++ b/lib/models/metadata/metadata.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index f54ee379..301929a5 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -595,7 +595,7 @@ SpotubeAudioLossyContainerQuality _$SpotubeAudioLossyContainerQualityFromJson( /// @nodoc mixin _$SpotubeAudioLossyContainerQuality { - double get bitrate => throw _privateConstructorUsedError; + int get bitrate => throw _privateConstructorUsedError; /// Serializes this SpotubeAudioLossyContainerQuality to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -615,7 +615,7 @@ abstract class $SpotubeAudioLossyContainerQualityCopyWith<$Res> { _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, SpotubeAudioLossyContainerQuality>; @useResult - $Res call({double bitrate}); + $Res call({int bitrate}); } /// @nodoc @@ -640,7 +640,7 @@ class _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, bitrate: null == bitrate ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable - as double, + as int, ) as $Val); } } @@ -654,7 +654,7 @@ abstract class _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res> __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>; @override @useResult - $Res call({double bitrate}); + $Res call({int bitrate}); } /// @nodoc @@ -678,7 +678,7 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> bitrate: null == bitrate ? _value.bitrate : bitrate // ignore: cast_nullable_to_non_nullable - as double, + as int, )); } } @@ -686,20 +686,15 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioLossyContainerQualityImpl - implements _SpotubeAudioLossyContainerQuality { - _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}); + extends _SpotubeAudioLossyContainerQuality { + _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}) : super._(); factory _$SpotubeAudioLossyContainerQualityImpl.fromJson( Map json) => _$$SpotubeAudioLossyContainerQualityImplFromJson(json); @override - final double bitrate; - - @override - String toString() { - return 'SpotubeAudioLossyContainerQuality(bitrate: $bitrate)'; - } + final int bitrate; @override bool operator ==(Object other) { @@ -732,16 +727,17 @@ class _$SpotubeAudioLossyContainerQualityImpl } abstract class _SpotubeAudioLossyContainerQuality - implements SpotubeAudioLossyContainerQuality { - factory _SpotubeAudioLossyContainerQuality({required final double bitrate}) = + extends SpotubeAudioLossyContainerQuality { + factory _SpotubeAudioLossyContainerQuality({required final int bitrate}) = _$SpotubeAudioLossyContainerQualityImpl; + _SpotubeAudioLossyContainerQuality._() : super._(); factory _SpotubeAudioLossyContainerQuality.fromJson( Map json) = _$SpotubeAudioLossyContainerQualityImpl.fromJson; @override - double get bitrate; + int get bitrate; /// Create a copy of SpotubeAudioLossyContainerQuality /// with the given fields replaced by the non-null parameter values. @@ -759,8 +755,8 @@ SpotubeAudioLosslessContainerQuality /// @nodoc mixin _$SpotubeAudioLosslessContainerQuality { - int get bitDepth => throw _privateConstructorUsedError; - double get sampleRate => throw _privateConstructorUsedError; + int get bitDepth => throw _privateConstructorUsedError; // bit + int get sampleRate => throw _privateConstructorUsedError; /// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -781,7 +777,7 @@ abstract class $SpotubeAudioLosslessContainerQualityCopyWith<$Res> { _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, SpotubeAudioLosslessContainerQuality>; @useResult - $Res call({int bitDepth, double sampleRate}); + $Res call({int bitDepth, int sampleRate}); } /// @nodoc @@ -811,7 +807,7 @@ class _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, sampleRate: null == sampleRate ? _value.sampleRate : sampleRate // ignore: cast_nullable_to_non_nullable - as double, + as int, ) as $Val); } } @@ -825,7 +821,7 @@ abstract class _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res> __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>; @override @useResult - $Res call({int bitDepth, double sampleRate}); + $Res call({int bitDepth, int sampleRate}); } /// @nodoc @@ -854,7 +850,7 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> sampleRate: null == sampleRate ? _value.sampleRate : sampleRate // ignore: cast_nullable_to_non_nullable - as double, + as int, )); } } @@ -862,9 +858,10 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioLosslessContainerQualityImpl - implements _SpotubeAudioLosslessContainerQuality { + extends _SpotubeAudioLosslessContainerQuality { _$SpotubeAudioLosslessContainerQualityImpl( - {required this.bitDepth, required this.sampleRate}); + {required this.bitDepth, required this.sampleRate}) + : super._(); factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson( Map json) => @@ -872,13 +869,9 @@ class _$SpotubeAudioLosslessContainerQualityImpl @override final int bitDepth; +// bit @override - final double sampleRate; - - @override - String toString() { - return 'SpotubeAudioLosslessContainerQuality(bitDepth: $bitDepth, sampleRate: $sampleRate)'; - } + final int sampleRate; @override bool operator ==(Object other) { @@ -914,19 +907,20 @@ class _$SpotubeAudioLosslessContainerQualityImpl } abstract class _SpotubeAudioLosslessContainerQuality - implements SpotubeAudioLosslessContainerQuality { + extends SpotubeAudioLosslessContainerQuality { factory _SpotubeAudioLosslessContainerQuality( - {required final int bitDepth, required final double sampleRate}) = + {required final int bitDepth, required final int sampleRate}) = _$SpotubeAudioLosslessContainerQualityImpl; + _SpotubeAudioLosslessContainerQuality._() : super._(); factory _SpotubeAudioLosslessContainerQuality.fromJson( Map json) = _$SpotubeAudioLosslessContainerQualityImpl.fromJson; @override - int get bitDepth; + int get bitDepth; // bit @override - double get sampleRate; + int get sampleRate; /// Create a copy of SpotubeAudioLosslessContainerQuality /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/metadata/metadata.g.dart b/lib/models/metadata/metadata.g.dart index 7497053c..56783d80 100644 --- a/lib/models/metadata/metadata.g.dart +++ b/lib/models/metadata/metadata.g.dart @@ -52,7 +52,7 @@ Map _$$SpotubeAudioSourceContainerPresetLosslessImplToJson( _$SpotubeAudioLossyContainerQualityImpl _$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) => _$SpotubeAudioLossyContainerQualityImpl( - bitrate: (json['bitrate'] as num).toDouble(), + bitrate: (json['bitrate'] as num).toInt(), ); Map _$$SpotubeAudioLossyContainerQualityImplToJson( @@ -65,7 +65,7 @@ _$SpotubeAudioLosslessContainerQualityImpl _$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) => _$SpotubeAudioLosslessContainerQualityImpl( bitDepth: (json['bitDepth'] as num).toInt(), - sampleRate: (json['sampleRate'] as num).toDouble(), + sampleRate: (json['sampleRate'] as num).toInt(), ); Map _$$SpotubeAudioLosslessContainerQualityImplToJson( diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index 262fcefa..677b34b8 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -1,7 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotube/models/metadata/metadata.dart'; -part 'track_sources.freezed.dart'; part 'track_sources.g.dart'; @JsonSerializable() diff --git a/lib/models/playback/track_sources.freezed.dart b/lib/models/playback/track_sources.freezed.dart deleted file mode 100644 index 09ceb399..00000000 --- a/lib/models/playback/track_sources.freezed.dart +++ /dev/null @@ -1,800 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'track_sources.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -TrackSourceQuery _$TrackSourceQueryFromJson(Map json) { - return _TrackSourceQuery.fromJson(json); -} - -/// @nodoc -mixin _$TrackSourceQuery { - String get id => throw _privateConstructorUsedError; - String get title => throw _privateConstructorUsedError; - List get artists => throw _privateConstructorUsedError; - String get album => throw _privateConstructorUsedError; - int get durationMs => throw _privateConstructorUsedError; - String get isrc => throw _privateConstructorUsedError; - bool get explicit => throw _privateConstructorUsedError; - - /// Serializes this TrackSourceQuery to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceQueryCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceQueryCopyWith<$Res> { - factory $TrackSourceQueryCopyWith( - TrackSourceQuery value, $Res Function(TrackSourceQuery) then) = - _$TrackSourceQueryCopyWithImpl<$Res, TrackSourceQuery>; - @useResult - $Res call( - {String id, - String title, - List artists, - String album, - int durationMs, - String isrc, - bool explicit}); -} - -/// @nodoc -class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery> - implements $TrackSourceQueryCopyWith<$Res> { - _$TrackSourceQueryCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? album = null, - Object? durationMs = null, - Object? isrc = null, - Object? explicit = null, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - album: null == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - isrc: null == isrc - ? _value.isrc - : isrc // ignore: cast_nullable_to_non_nullable - as String, - explicit: null == explicit - ? _value.explicit - : explicit // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceQueryImplCopyWith<$Res> - implements $TrackSourceQueryCopyWith<$Res> { - factory _$$TrackSourceQueryImplCopyWith(_$TrackSourceQueryImpl value, - $Res Function(_$TrackSourceQueryImpl) then) = - __$$TrackSourceQueryImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String id, - String title, - List artists, - String album, - int durationMs, - String isrc, - bool explicit}); -} - -/// @nodoc -class __$$TrackSourceQueryImplCopyWithImpl<$Res> - extends _$TrackSourceQueryCopyWithImpl<$Res, _$TrackSourceQueryImpl> - implements _$$TrackSourceQueryImplCopyWith<$Res> { - __$$TrackSourceQueryImplCopyWithImpl(_$TrackSourceQueryImpl _value, - $Res Function(_$TrackSourceQueryImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? album = null, - Object? durationMs = null, - Object? isrc = null, - Object? explicit = null, - }) { - return _then(_$TrackSourceQueryImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value._artists - : artists // ignore: cast_nullable_to_non_nullable - as List, - album: null == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - isrc: null == isrc - ? _value.isrc - : isrc // ignore: cast_nullable_to_non_nullable - as String, - explicit: null == explicit - ? _value.explicit - : explicit // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceQueryImpl extends _TrackSourceQuery { - _$TrackSourceQueryImpl( - {required this.id, - required this.title, - required final List artists, - required this.album, - required this.durationMs, - required this.isrc, - required this.explicit}) - : _artists = artists, - super._(); - - factory _$TrackSourceQueryImpl.fromJson(Map json) => - _$$TrackSourceQueryImplFromJson(json); - - @override - final String id; - @override - final String title; - final List _artists; - @override - List get artists { - if (_artists is EqualUnmodifiableListView) return _artists; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_artists); - } - - @override - final String album; - @override - final int durationMs; - @override - final String isrc; - @override - final bool explicit; - - @override - String toString() { - return 'TrackSourceQuery(id: $id, title: $title, artists: $artists, album: $album, durationMs: $durationMs, isrc: $isrc, explicit: $explicit)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceQueryImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.title, title) || other.title == title) && - const DeepCollectionEquality().equals(other._artists, _artists) && - (identical(other.album, album) || other.album == album) && - (identical(other.durationMs, durationMs) || - other.durationMs == durationMs) && - (identical(other.isrc, isrc) || other.isrc == isrc) && - (identical(other.explicit, explicit) || - other.explicit == explicit)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - id, - title, - const DeepCollectionEquality().hash(_artists), - album, - durationMs, - isrc, - explicit); - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => - __$$TrackSourceQueryImplCopyWithImpl<_$TrackSourceQueryImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceQueryImplToJson( - this, - ); - } -} - -abstract class _TrackSourceQuery extends TrackSourceQuery { - factory _TrackSourceQuery( - {required final String id, - required final String title, - required final List artists, - required final String album, - required final int durationMs, - required final String isrc, - required final bool explicit}) = _$TrackSourceQueryImpl; - _TrackSourceQuery._() : super._(); - - factory _TrackSourceQuery.fromJson(Map json) = - _$TrackSourceQueryImpl.fromJson; - - @override - String get id; - @override - String get title; - @override - List get artists; - @override - String get album; - @override - int get durationMs; - @override - String get isrc; - @override - bool get explicit; - - /// Create a copy of TrackSourceQuery - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => - throw _privateConstructorUsedError; -} - -TrackSourceInfo _$TrackSourceInfoFromJson(Map json) { - return _TrackSourceInfo.fromJson(json); -} - -/// @nodoc -mixin _$TrackSourceInfo { - String get id => throw _privateConstructorUsedError; - String get title => throw _privateConstructorUsedError; - String get artists => throw _privateConstructorUsedError; - String get thumbnail => throw _privateConstructorUsedError; - String get pageUrl => throw _privateConstructorUsedError; - int get durationMs => throw _privateConstructorUsedError; - - /// Serializes this TrackSourceInfo to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceInfoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceInfoCopyWith<$Res> { - factory $TrackSourceInfoCopyWith( - TrackSourceInfo value, $Res Function(TrackSourceInfo) then) = - _$TrackSourceInfoCopyWithImpl<$Res, TrackSourceInfo>; - @useResult - $Res call( - {String id, - String title, - String artists, - String thumbnail, - String pageUrl, - int durationMs}); -} - -/// @nodoc -class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo> - implements $TrackSourceInfoCopyWith<$Res> { - _$TrackSourceInfoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? thumbnail = null, - Object? pageUrl = null, - Object? durationMs = null, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as String, - thumbnail: null == thumbnail - ? _value.thumbnail - : thumbnail // ignore: cast_nullable_to_non_nullable - as String, - pageUrl: null == pageUrl - ? _value.pageUrl - : pageUrl // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceInfoImplCopyWith<$Res> - implements $TrackSourceInfoCopyWith<$Res> { - factory _$$TrackSourceInfoImplCopyWith(_$TrackSourceInfoImpl value, - $Res Function(_$TrackSourceInfoImpl) then) = - __$$TrackSourceInfoImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String id, - String title, - String artists, - String thumbnail, - String pageUrl, - int durationMs}); -} - -/// @nodoc -class __$$TrackSourceInfoImplCopyWithImpl<$Res> - extends _$TrackSourceInfoCopyWithImpl<$Res, _$TrackSourceInfoImpl> - implements _$$TrackSourceInfoImplCopyWith<$Res> { - __$$TrackSourceInfoImplCopyWithImpl( - _$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? title = null, - Object? artists = null, - Object? thumbnail = null, - Object? pageUrl = null, - Object? durationMs = null, - }) { - return _then(_$TrackSourceInfoImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - artists: null == artists - ? _value.artists - : artists // ignore: cast_nullable_to_non_nullable - as String, - thumbnail: null == thumbnail - ? _value.thumbnail - : thumbnail // ignore: cast_nullable_to_non_nullable - as String, - pageUrl: null == pageUrl - ? _value.pageUrl - : pageUrl // ignore: cast_nullable_to_non_nullable - as String, - durationMs: null == durationMs - ? _value.durationMs - : durationMs // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceInfoImpl implements _TrackSourceInfo { - _$TrackSourceInfoImpl( - {required this.id, - required this.title, - required this.artists, - required this.thumbnail, - required this.pageUrl, - required this.durationMs}); - - factory _$TrackSourceInfoImpl.fromJson(Map json) => - _$$TrackSourceInfoImplFromJson(json); - - @override - final String id; - @override - final String title; - @override - final String artists; - @override - final String thumbnail; - @override - final String pageUrl; - @override - final int durationMs; - - @override - String toString() { - return 'TrackSourceInfo(id: $id, title: $title, artists: $artists, thumbnail: $thumbnail, pageUrl: $pageUrl, durationMs: $durationMs)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceInfoImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.title, title) || other.title == title) && - (identical(other.artists, artists) || other.artists == artists) && - (identical(other.thumbnail, thumbnail) || - other.thumbnail == thumbnail) && - (identical(other.pageUrl, pageUrl) || other.pageUrl == pageUrl) && - (identical(other.durationMs, durationMs) || - other.durationMs == durationMs)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, id, title, artists, thumbnail, pageUrl, durationMs); - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => - __$$TrackSourceInfoImplCopyWithImpl<_$TrackSourceInfoImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceInfoImplToJson( - this, - ); - } -} - -abstract class _TrackSourceInfo implements TrackSourceInfo { - factory _TrackSourceInfo( - {required final String id, - required final String title, - required final String artists, - required final String thumbnail, - required final String pageUrl, - required final int durationMs}) = _$TrackSourceInfoImpl; - - factory _TrackSourceInfo.fromJson(Map json) = - _$TrackSourceInfoImpl.fromJson; - - @override - String get id; - @override - String get title; - @override - String get artists; - @override - String get thumbnail; - @override - String get pageUrl; - @override - int get durationMs; - - /// Create a copy of TrackSourceInfo - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => - throw _privateConstructorUsedError; -} - -TrackSource _$TrackSourceFromJson(Map json) { - return _TrackSource.fromJson(json); -} - -/// @nodoc -mixin _$TrackSource { - String get url => throw _privateConstructorUsedError; - SourceQualities get quality => throw _privateConstructorUsedError; - SourceCodecs get codec => throw _privateConstructorUsedError; - String get bitrate => throw _privateConstructorUsedError; - String get qualityLabel => throw _privateConstructorUsedError; - - /// Serializes this TrackSource to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $TrackSourceCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $TrackSourceCopyWith<$Res> { - factory $TrackSourceCopyWith( - TrackSource value, $Res Function(TrackSource) then) = - _$TrackSourceCopyWithImpl<$Res, TrackSource>; - @useResult - $Res call( - {String url, - SourceQualities quality, - SourceCodecs codec, - String bitrate, - String qualityLabel}); -} - -/// @nodoc -class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource> - implements $TrackSourceCopyWith<$Res> { - _$TrackSourceCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? quality = null, - Object? codec = null, - Object? bitrate = null, - Object? qualityLabel = null, - }) { - return _then(_value.copyWith( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - quality: null == quality - ? _value.quality - : quality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - codec: null == codec - ? _value.codec - : codec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - bitrate: null == bitrate - ? _value.bitrate - : bitrate // ignore: cast_nullable_to_non_nullable - as String, - qualityLabel: null == qualityLabel - ? _value.qualityLabel - : qualityLabel // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$TrackSourceImplCopyWith<$Res> - implements $TrackSourceCopyWith<$Res> { - factory _$$TrackSourceImplCopyWith( - _$TrackSourceImpl value, $Res Function(_$TrackSourceImpl) then) = - __$$TrackSourceImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String url, - SourceQualities quality, - SourceCodecs codec, - String bitrate, - String qualityLabel}); -} - -/// @nodoc -class __$$TrackSourceImplCopyWithImpl<$Res> - extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl> - implements _$$TrackSourceImplCopyWith<$Res> { - __$$TrackSourceImplCopyWithImpl( - _$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then) - : super(_value, _then); - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? quality = null, - Object? codec = null, - Object? bitrate = null, - Object? qualityLabel = null, - }) { - return _then(_$TrackSourceImpl( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - quality: null == quality - ? _value.quality - : quality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - codec: null == codec - ? _value.codec - : codec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - bitrate: null == bitrate - ? _value.bitrate - : bitrate // ignore: cast_nullable_to_non_nullable - as String, - qualityLabel: null == qualityLabel - ? _value.qualityLabel - : qualityLabel // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$TrackSourceImpl implements _TrackSource { - _$TrackSourceImpl( - {required this.url, - required this.quality, - required this.codec, - required this.bitrate, - required this.qualityLabel}); - - factory _$TrackSourceImpl.fromJson(Map json) => - _$$TrackSourceImplFromJson(json); - - @override - final String url; - @override - final SourceQualities quality; - @override - final SourceCodecs codec; - @override - final String bitrate; - @override - final String qualityLabel; - - @override - String toString() { - return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TrackSourceImpl && - (identical(other.url, url) || other.url == url) && - (identical(other.quality, quality) || other.quality == quality) && - (identical(other.codec, codec) || other.codec == codec) && - (identical(other.bitrate, bitrate) || other.bitrate == bitrate) && - (identical(other.qualityLabel, qualityLabel) || - other.qualityLabel == qualityLabel)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel); - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => - __$$TrackSourceImplCopyWithImpl<_$TrackSourceImpl>(this, _$identity); - - @override - Map toJson() { - return _$$TrackSourceImplToJson( - this, - ); - } -} - -abstract class _TrackSource implements TrackSource { - factory _TrackSource( - {required final String url, - required final SourceQualities quality, - required final SourceCodecs codec, - required final String bitrate, - required final String qualityLabel}) = _$TrackSourceImpl; - - factory _TrackSource.fromJson(Map json) = - _$TrackSourceImpl.fromJson; - - @override - String get url; - @override - SourceQualities get quality; - @override - SourceCodecs get codec; - @override - String get bitrate; - @override - String get qualityLabel; - - /// Create a copy of TrackSource - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/models/playback/track_sources.g.dart b/lib/models/playback/track_sources.g.dart index dd63aebb..3088493a 100644 --- a/lib/models/playback/track_sources.g.dart +++ b/lib/models/playback/track_sources.g.dart @@ -7,17 +7,18 @@ part of 'track_sources.dart'; // ************************************************************************** BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( - query: TrackSourceQuery.fromJson( + query: SpotubeFullTrackObject.fromJson( Map.from(json['query'] as Map)), - source: $enumDecode(_$AudioSourceEnumMap, json['source']), - info: TrackSourceInfo.fromJson( + source: json['source'] as String, + info: SpotubeAudioSourceMatchObject.fromJson( Map.from(json['info'] as Map)), sources: (json['sources'] as List) - .map((e) => TrackSource.fromJson(Map.from(e as Map))) + .map((e) => SpotubeAudioSourceStreamObject.fromJson( + Map.from(e as Map))) .toList(), siblings: (json['siblings'] as List?) - ?.map((e) => - TrackSourceInfo.fromJson(Map.from(e as Map))) + ?.map((e) => SpotubeAudioSourceMatchObject.fromJson( + Map.from(e as Map))) .toList() ?? const [], ); @@ -25,92 +26,8 @@ BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( Map _$BasicSourcedTrackToJson(BasicSourcedTrack instance) => { 'query': instance.query.toJson(), - 'source': _$AudioSourceEnumMap[instance.source]!, 'info': instance.info.toJson(), + 'source': instance.source, 'sources': instance.sources.map((e) => e.toJson()).toList(), 'siblings': instance.siblings.map((e) => e.toJson()).toList(), }; - -const _$AudioSourceEnumMap = { - AudioSource.youtube: 'youtube', - AudioSource.piped: 'piped', - AudioSource.jiosaavn: 'jiosaavn', - AudioSource.invidious: 'invidious', - AudioSource.dabMusic: 'dabMusic', -}; - -_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) => - _$TrackSourceQueryImpl( - id: json['id'] as String, - title: json['title'] as String, - artists: - (json['artists'] as List).map((e) => e as String).toList(), - album: json['album'] as String, - durationMs: (json['durationMs'] as num).toInt(), - isrc: json['isrc'] as String, - explicit: json['explicit'] as bool, - ); - -Map _$$TrackSourceQueryImplToJson( - _$TrackSourceQueryImpl instance) => - { - 'id': instance.id, - 'title': instance.title, - 'artists': instance.artists, - 'album': instance.album, - 'durationMs': instance.durationMs, - 'isrc': instance.isrc, - 'explicit': instance.explicit, - }; - -_$TrackSourceInfoImpl _$$TrackSourceInfoImplFromJson(Map json) => - _$TrackSourceInfoImpl( - id: json['id'] as String, - title: json['title'] as String, - artists: json['artists'] as String, - thumbnail: json['thumbnail'] as String, - pageUrl: json['pageUrl'] as String, - durationMs: (json['durationMs'] as num).toInt(), - ); - -Map _$$TrackSourceInfoImplToJson( - _$TrackSourceInfoImpl instance) => - { - 'id': instance.id, - 'title': instance.title, - 'artists': instance.artists, - 'thumbnail': instance.thumbnail, - 'pageUrl': instance.pageUrl, - 'durationMs': instance.durationMs, - }; - -_$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl( - url: json['url'] as String, - quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']), - codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']), - bitrate: json['bitrate'] as String, - qualityLabel: json['qualityLabel'] as String, - ); - -Map _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => - { - 'url': instance.url, - 'quality': _$SourceQualitiesEnumMap[instance.quality]!, - 'codec': _$SourceCodecsEnumMap[instance.codec]!, - 'bitrate': instance.bitrate, - 'qualityLabel': instance.qualityLabel, - }; - -const _$SourceQualitiesEnumMap = { - SourceQualities.uncompressed: 'uncompressed', - SourceQualities.high: 'high', - SourceQualities.medium: 'medium', - SourceQualities.low: 'low', -}; - -const _$SourceCodecsEnumMap = { - SourceCodecs.m4a: 'm4a', - SourceCodecs.weba: 'weba', - SourceCodecs.mp3: 'mp3', - SourceCodecs.flac: 'flac', -}; diff --git a/lib/modules/library/local_folder/cache_export_dialog.dart b/lib/modules/library/local_folder/cache_export_dialog.dart index fde219c9..4c86a8d5 100644 --- a/lib/modules/library/local_folder/cache_export_dialog.dart +++ b/lib/modules/library/local_folder/cache_export_dialog.dart @@ -7,7 +7,7 @@ import 'package:path/path.dart' as path; import 'package:spotube/extensions/context.dart'; import 'package:spotube/services/logger/logger.dart'; -final codecs = SourceCodecs.values.map((s) => s.name); +const containers = ["m4a", "mp3", "mp4", "ogg", "wav", "flac"]; class LocalFolderCacheExportDialog extends HookConsumerWidget { final Directory exportDir; @@ -29,7 +29,8 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget { final stream = cacheDir.list().where( (event) => event is File && - codecs.contains(path.extension(event.path).replaceAll(".", "")), + containers + .contains(path.extension(event.path).replaceAll(".", "")), ); stream.listen( diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 69262641..5ea690e0 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -21,11 +21,9 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_label.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/volume_provider.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; - -import 'package:url_launcher/url_launcher_string.dart'; class PlayerView extends HookConsumerWidget { final PanelController panelController; @@ -45,14 +43,7 @@ class PlayerView extends HookConsumerWidget { final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final mediaQuery = MediaQuery.sizeOf(context); - - final activeSourceCodec = useMemoized( - () { - return currentActiveTrackSource - ?.getStreamOfCodec(currentActiveTrackSource.codec); - }, - [currentActiveTrackSource?.sources, currentActiveTrackSource?.codec], - ); + final qualityLabel = ref.watch(audioSourceQualityLabelProvider); final shouldHide = useState(true); @@ -117,22 +108,6 @@ class PlayerView extends HookConsumerWidget { ) ], trailing: [ - if (currentActiveTrackSource is YoutubeSourcedTrack) - TextButton( - size: const ButtonSize(1.2), - leading: Assets.images.logos.songlinkTransparent.image( - width: 20, - height: 20, - color: theme.colorScheme.foreground, - ), - onPressed: () { - final url = - "https://song.link/s/${currentActiveTrack?.id}"; - - launchUrlString(url); - }, - child: Text(context.l10n.song_link), - ), if (!isLocalTrack) Tooltip( tooltip: TooltipContainer( @@ -276,20 +251,19 @@ class PlayerView extends HookConsumerWidget { }), ), const Gap(25), - if (activeSourceCodec != null) - OutlineBadge( - style: const ButtonStyle.outline( - size: ButtonSize.normal, - density: ButtonDensity.dense, - shape: ButtonShape.rectangle, - ).copyWith( - textStyle: (context, states, value) { - return value.copyWith(fontWeight: FontWeight.w500); - }, - ), - leading: const Icon(SpotubeIcons.lightningOutlined), - child: Text(activeSourceCodec.qualityLabel), - ) + OutlineBadge( + style: const ButtonStyle.outline( + size: ButtonSize.normal, + density: ButtonDensity.dense, + shape: ButtonShape.rectangle, + ).copyWith( + textStyle: (context, states, value) { + return value.copyWith(fontWeight: FontWeight.w500); + }, + ), + leading: const Icon(SpotubeIcons.lightningOutlined), + child: Text(qualityLabel), + ) ], ), ), diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index a6c3ae32..7b780143 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -1,60 +1,16 @@ -import 'package:collection/collection.dart'; - import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; -import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; -import 'package:spotube/hooks/utils/use_debounce.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/youtube_engine/youtube_engine.dart'; -import 'package:spotube/services/sourced_track/models/video_info.dart'; -import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; -import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final sourceInfoToIconMap = { - AudioSource.youtube: - const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), - AudioSource.jiosaavn: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - image: DecorationImage( - image: Assets.images.logos.jiosaavn.provider(), - fit: BoxFit.cover, - ), - ), - ), - AudioSource.piped: const Icon(SpotubeIcons.piped), - AudioSource.invidious: Container( - height: 18, - width: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - image: DecorationImage( - image: Assets.images.logos.invidious.provider(), - fit: BoxFit.cover, - ), - ), - ), -}; class SiblingTracksSheet extends HookConsumerWidget { final bool floating; @@ -65,94 +21,21 @@ class SiblingTracksSheet extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final theme = Theme.of(context); - final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final preferences = ref.watch(userPreferencesProvider); - final youtubeEngine = ref.watch(youtubeEngineProvider); + final controller = useScrollController(); - final isLoading = useState(false); - final isSearching = useState(false); - final searchMode = useState(preferences.searchMode); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final activeTrackSources = ref.watch(activeTrackSourcesProvider); final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; final activeTrack = activeTrackSources.asData?.value?.track; final activeTrackSource = activeTrackSources.asData?.value?.source; - final title = ServiceUtils.getTitle( - activeTrack?.name ?? "", - artists: activeTrack?.artists.map((e) => e.name).toList() ?? [], - onlyCleanArtist: true, - ).trim(); - - final defaultSearchTerm = - "$title - ${activeTrack?.artists.asString() ?? ""}"; - final searchController = useShadcnTextEditingController( - text: defaultSearchTerm, - ); - - final searchTerm = useDebounce( - useValueListenable(searchController).text, - ); - - final controller = useScrollController(); - - final searchRequest = useMemoized(() async { - if (searchTerm.trim().isEmpty || activeTrackSource == null) { - return []; - } - if (preferences.audioSource == AudioSource.jiosaavn) { - final resultsJioSaavn = - await jiosaavnClient.search.songs(searchTerm.trim()); - final results = await Future.wait( - resultsJioSaavn.results.mapIndexed((i, song) async { - final siblingType = JioSaavnSourcedTrack.toSiblingType(song); - return siblingType.info; - })); - - final activeSourceInfo = activeTrackSource.info; - - return results - ..removeWhere((element) => element.id == activeSourceInfo.id) - ..insert( - 0, - activeSourceInfo, - ); - } else { - final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim()); - - final searchResults = await Future.wait( - resultsYt - .map(YoutubeVideoInfo.fromVideo) - .mapIndexed((i, video) async { - if (!context.mounted) return null; - final siblingType = - await YoutubeSourcedTrack.toSiblingType(i, video, ref); - return siblingType.info; - }) - .whereType>() - .toList(), - ); - final activeSourceInfo = activeTrackSource.info; - return searchResults - ..removeWhere((element) => element.id == activeSourceInfo.id) - ..insert(0, activeSourceInfo); - } - }, [ - searchTerm, - searchMode.value, - activeTrack, - activeTrackSource, - preferences.audioSource, - youtubeEngine, - ]); - - final siblings = useMemoized( + final siblings = useMemoized>( () => !isFetchingActiveTrack ? [ if (activeTrackSource != null) activeTrackSource.info, ...?activeTrackSource?.siblings, ] - : [], + : [], [activeTrackSource, isFetchingActiveTrack], ); @@ -166,74 +49,6 @@ class SiblingTracksSheet extends HookConsumerWidget { return null; }, [activeTrack, previousActiveTrack]); - final itemBuilder = useCallback( - (TrackSourceInfo sourceInfo, AudioSource source) { - final icon = sourceInfoToIconMap[source]; - return ButtonTile( - style: ButtonVariance.ghost, - padding: const EdgeInsets.symmetric(horizontal: 8), - title: Text( - sourceInfo.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - leading: UniversalImage( - path: sourceInfo.thumbnail, - height: 60, - width: 60, - ), - trailing: Text(Duration(milliseconds: sourceInfo.durationMs) - .toHumanReadableString()), - subtitle: Row( - children: [ - if (icon != null) icon, - Flexible( - child: Text( - " • ${sourceInfo.artists}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - enabled: !isFetchingActiveTrack && !isLoading.value, - selected: !isFetchingActiveTrack && - sourceInfo.id == activeTrackSource?.info.id, - onPressed: () async { - if (!isFetchingActiveTrack && - sourceInfo.id != activeTrackSource?.info.id) { - try { - isLoading.value = true; - await activeTrackNotifier?.swapWithSibling(sourceInfo); - await ref.read(audioPlayerProvider.notifier).swapActiveSource(); - - if (context.mounted) { - if (MediaQuery.sizeOf(context).mdAndUp) { - closeOverlay(context); - } else { - closeDrawer(context); - } - } - } finally { - if (context.mounted) { - isLoading.value = false; - } - } - } - }, - ); - }, - [ - activeTrackSource, - activeTrackNotifier, - siblings, - isFetchingActiveTrack, - isLoading.value, - ], - ); - - final scale = context.theme.scaling; - return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, @@ -244,72 +59,16 @@ class SiblingTracksSheet extends HookConsumerWidget { spacing: 5, children: [ AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: !isSearching.value - ? Text( - context.l10n.alternative_track_sources, - ).bold() - : ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 320 * scale, - maxHeight: 38 * scale, - ), - child: TextField( - autofocus: true, - controller: searchController, - placeholder: Text(context.l10n.search), - style: theme.typography.bold, - ), - ), - ), - const Spacer(), - if (!isSearching.value) ...[ - IconButton.outline( - icon: const Icon(SpotubeIcons.search, size: 18), - onPressed: () { - isSearching.value = true; - }, - ), - if (!floating) const BackButton(icon: SpotubeIcons.angleDown) - ] else ...[ - if (preferences.audioSource == AudioSource.piped) - IconButton.outline( - icon: const Icon(SpotubeIcons.filter, size: 18), - onPressed: () { - showPopover( - context: context, - alignment: Alignment.bottomRight, - builder: (context) { - return DropdownMenu( - children: SearchMode.values - .map( - (e) => MenuButton( - onPressed: (context) { - searchMode.value = e; - }, - enabled: searchMode.value != e, - child: Text(e.label), - ), - ) - .toList(), - ); - }, - ); - }, - ), - IconButton.outline( - icon: const Icon(SpotubeIcons.close, size: 18), - onPressed: () { - isSearching.value = false; - }, - ), - ] + duration: const Duration(milliseconds: 300), + child: Text( + context.l10n.alternative_track_sources, + ).bold()), ], ), ), AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: isLoading.value + child: activeTrackSources.isLoading ? const SizedBox( width: double.infinity, child: LinearProgressIndicator(), @@ -323,42 +82,62 @@ class SiblingTracksSheet extends HookConsumerWidget { FadeTransition(opacity: animation, child: child), child: InterScrollbar( controller: controller, - child: switch (isSearching.value) { - false => ListView.separated( - padding: const EdgeInsets.all(8.0), - controller: controller, - itemCount: siblings.length, - separatorBuilder: (context, index) => const Gap(8), - itemBuilder: (context, index) => itemBuilder( - siblings[index], - activeTrackSource!.source, - ), - ), - true => FutureBuilder( - future: searchRequest, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text(snapshot.error.toString()), - ); - } else if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator()); - } + child: ListView.separated( + padding: const EdgeInsets.all(8.0), + controller: controller, + itemCount: siblings.length, + separatorBuilder: (context, index) => const Gap(8), + itemBuilder: (context, index) { + final sourceInfo = siblings[index]; - return ListView.separated( - padding: const EdgeInsets.all(8.0), - controller: controller, - itemCount: snapshot.data!.length, - separatorBuilder: (context, index) => const Gap(8), - itemBuilder: (context, index) => itemBuilder( - snapshot.data![index], - preferences.audioSource, - ), - ); + return ButtonTile( + style: ButtonVariance.ghost, + padding: const EdgeInsets.symmetric(horizontal: 8), + title: Text( + sourceInfo.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + leading: sourceInfo.thumbnail != null + ? UniversalImage( + path: sourceInfo.thumbnail!, + height: 60, + width: 60, + ) + : null, + trailing: + Text(sourceInfo.duration.toHumanReadableString()), + subtitle: Flexible( + child: Text( + sourceInfo.artists.join(", "), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + enabled: !isFetchingActiveTrack, + selected: !isFetchingActiveTrack && + sourceInfo.id == activeTrackSource?.info.id, + onPressed: () async { + if (!isFetchingActiveTrack && + sourceInfo.id != activeTrackSource?.info.id) { + await activeTrackNotifier + ?.swapWithSibling(sourceInfo); + await ref + .read(audioPlayerProvider.notifier) + .swapActiveSource(); + + if (context.mounted) { + if (MediaQuery.sizeOf(context).mdAndUp) { + closeOverlay(context); + } else { + closeDrawer(context); + } + } + } }, - ), - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index a6f887cb..699024b1 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -1,32 +1,11 @@ -import 'package:flutter/material.dart' show Badge; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/ui/button_tile.dart'; -import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -final audioSourceToIconMap = { - AudioSource.youtube: const Icon( - SpotubeIcons.youtube, - color: Colors.red, - size: 20, - ), - AudioSource.piped: const Icon(SpotubeIcons.piped, size: 20), - AudioSource.invidious: ClipRRect( - borderRadius: BorderRadius.circular(26), - child: Assets.images.logos.invidious.image(width: 26, height: 26), - ), - AudioSource.jiosaavn: - Assets.images.logos.jiosaavn.image(width: 20, height: 20), - AudioSource.dabMusic: - Assets.images.logos.dabMusic.image(width: 20, height: 20), -}; - class GettingStartedPagePlaybackSection extends HookConsumerWidget { final VoidCallback onNext; final VoidCallback onPrevious; @@ -42,19 +21,19 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { final preferences = ref.watch(userPreferencesProvider); final preferencesNotifier = ref.read(userPreferencesProvider.notifier); - final audioSourceToDescription = useMemoized( - () => { - AudioSource.youtube: "${context.l10n.youtube_source_description}\n" - "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", - AudioSource.piped: context.l10n.piped_source_description, - AudioSource.jiosaavn: - "${context.l10n.jiosaavn_source_description}\n" - "${context.l10n.highest_quality("320kbps mp4")}", - AudioSource.invidious: context.l10n.invidious_source_description, - AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" - "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", - }, - []); + // final audioSourceToDescription = useMemoized( + // () => { + // AudioSource.youtube: "${context.l10n.youtube_source_description}\n" + // "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", + // AudioSource.piped: context.l10n.piped_source_description, + // AudioSource.jiosaavn: + // "${context.l10n.jiosaavn_source_description}\n" + // "${context.l10n.highest_quality("320kbps mp4")}", + // AudioSource.invidious: context.l10n.invidious_source_description, + // AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" + // "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", + // }, + // []); return Center( child: BlurCard( @@ -69,44 +48,44 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget { ], ), const Gap(16), - Align( - alignment: Alignment.centerLeft, - child: Text(context.l10n.select_audio_source).semiBold().large(), - ), - const Gap(16), - RadioGroup( - value: preferences.audioSource, - onChanged: (value) { - preferencesNotifier.setAudioSource(value); - }, - child: Wrap( - spacing: 6, - runSpacing: 6, - children: [ - for (final source in AudioSource.values) - Badge( - isLabelVisible: source == AudioSource.dabMusic, - label: const Text("NEW"), - backgroundColor: Colors.lime[300], - textColor: Colors.black, - child: RadioCard( - value: source, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - audioSourceToIconMap[source]!, - Text(source.label), - ], - ), - ), - ), - ], - ), - ), - const Gap(16), - Text( - audioSourceToDescription[preferences.audioSource]!, - ).small().muted(), + // Align( + // alignment: Alignment.centerLeft, + // child: Text(context.l10n.select_audio_source).semiBold().large(), + // ), + // const Gap(16), + // RadioGroup( + // value: preferences.audioSource, + // onChanged: (value) { + // preferencesNotifier.setAudioSource(value); + // }, + // child: Wrap( + // spacing: 6, + // runSpacing: 6, + // children: [ + // for (final source in AudioSource.values) + // Badge( + // isLabelVisible: source == AudioSource.dabMusic, + // label: const Text("NEW"), + // backgroundColor: Colors.lime[300], + // textColor: Colors.black, + // child: RadioCard( + // value: source, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // audioSourceToIconMap[source]!, + // Text(source.label), + // ], + // ), + // ), + // ), + // ], + // ), + // ), + // const Gap(16), + // Text( + // audioSourceToDescription[preferences.audioSource]!, + // ).small().muted(), const Gap(16), ButtonTile( title: Text(context.l10n.endless_playback), diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 77eaa0c5..29a8c2ea 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -1,29 +1,18 @@ -import 'dart:io'; - import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show ListTile; -import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; -import 'package:spotube/modules/settings/playback/edit_instance_url_dialog.dart'; -import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; +import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; -import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart'; -import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/kv_store/kv_store.dart'; -import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; class SettingsPlaybackSection extends HookConsumerWidget { @@ -33,332 +22,78 @@ class SettingsPlaybackSection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); + final sourcePresets = ref.watch(audioSourcePresetsProvider); + final sourcePresetsNotifier = + ref.watch(audioSourcePresetsProvider.notifier); final theme = Theme.of(context); return SectionCardWithHeading( heading: context.l10n.playback, children: [ - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.audioQuality), - title: Text(context.l10n.audio_quality), - value: preferences.audioQuality, - options: [ - if (preferences.audioSource == AudioSource.dabMusic) - SelectItemButton( - value: SourceQualities.uncompressed, - child: Text(context.l10n.uncompressed), - ), - SelectItemButton( - value: SourceQualities.high, - child: Text(context.l10n.high), - ), - if (preferences.audioSource != AudioSource.dabMusic) ...[ - SelectItemButton( - value: SourceQualities.medium, - child: Text(context.l10n.medium), - ), - SelectItemButton( - value: SourceQualities.low, - child: Text(context.l10n.low), - ), - ] - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setAudioQuality(value); - } - }, - ), - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: Text(context.l10n.audio_source), - value: preferences.audioSource, - options: AudioSource.values - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setAudioSource(value); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource != AudioSource.piped - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: const SizedBox.shrink(), - secondChild: Consumer( - builder: (context, ref, child) { - final instanceList = ref.watch(pipedInstancesFutureProvider); - - return instanceList.when( - data: (data) { - return AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.piped), - title: Text(context.l10n.piped_instance), - subtitle: Text( - "${context.l10n.piped_description}\n" - "${context.l10n.piped_warning}", - ), - value: preferences.pipedInstance, - showValueWhenUnfolded: false, - trailing: [ - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.add_custom_url), - ).call, - child: IconButton.outline( - icon: const Icon(SpotubeIcons.edit), - size: ButtonSize.small, - onPressed: () { - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.5), - builder: (context) => - SettingsPlaybackEditInstanceUrlDialog( - title: context.l10n.piped_instance, - initialValue: preferences.pipedInstance, - onSave: (value) { - preferencesNotifier.setPipedInstance(value); - }, - ), - ); - }, - ), - ) - ], - options: [ - if (data - .none((e) => e.apiUrl == preferences.pipedInstance)) - SelectItemButton( - value: preferences.pipedInstance, - child: Text.rich( - TextSpan( - style: theme.typography.xSmall.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan(text: context.l10n.custom), - const TextSpan(text: "\n"), - TextSpan(text: preferences.pipedInstance), - ], - ), - ), - ), - for (final e in data.sortedBy((e) => e.name)) - SelectItemButton( - value: e.apiUrl, - child: RichText( - text: TextSpan( - style: theme.typography.normal.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan( - text: "${e.name.trim()}\n", - ), - TextSpan( - text: e.locations - .map(countryCodeToEmoji) - .join(""), - style: GoogleFonts.notoColorEmoji(), - ), - ], - ), - ), - ), - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setPipedInstance(value); - } - }, - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Text(error.toString()), - ); + if (sourcePresets.presets.isNotEmpty) ...[ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: Text(context.l10n.streaming_music_codec), + value: sourcePresets.selectedStreamingContainerIndex, + options: [ + for (final MapEntry(:key, value: preset) + in sourcePresets.presets.asMap().entries) + SelectItemButton(value: key, child: Text(preset.name)), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingContainerIndex(value); }, ), - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource != AudioSource.invidious - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: const SizedBox.shrink(), - secondChild: Consumer( - builder: (context, ref, child) { - final instanceList = ref.watch(invidiousInstancesProvider); - - return instanceList.when( - data: (data) { - return AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.piped), - title: Text(context.l10n.invidious_instance), - subtitle: Text( - "${context.l10n.invidious_description}\n" - "${context.l10n.invidious_warning}", - ), - trailing: [ - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.add_custom_url), - ).call, - child: IconButton.outline( - icon: const Icon(SpotubeIcons.edit), - size: ButtonSize.small, - onPressed: () { - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.5), - builder: (context) => - SettingsPlaybackEditInstanceUrlDialog( - title: context.l10n.invidious_instance, - initialValue: preferences.invidiousInstance, - onSave: (value) { - preferencesNotifier - .setInvidiousInstance(value); - }, - ), - ); - }, - ), - ) - ], - value: preferences.invidiousInstance, - showValueWhenUnfolded: false, - options: [ - if (data.none((e) => - e.details.uri == preferences.invidiousInstance)) - SelectItemButton( - value: preferences.invidiousInstance, - child: Text.rich( - TextSpan( - style: theme.typography.xSmall.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan(text: context.l10n.custom), - const TextSpan(text: "\n"), - TextSpan(text: preferences.invidiousInstance), - ], - ), - ), - ), - for (final e in data.sortedBy((e) => e.name)) - SelectItemButton( - value: e.details.uri, - child: RichText( - text: TextSpan( - style: theme.typography.normal.copyWith( - color: theme.colorScheme.foreground, - ), - children: [ - TextSpan( - text: "${e.name.trim()}\n", - ), - TextSpan( - text: countryCodeToEmoji( - e.details.region, - ), - style: GoogleFonts.notoColorEmoji(), - ), - ], - ), - ), - ), - ], - onChanged: (value) { - if (value != null) { - preferencesNotifier.setInvidiousInstance(value); - } - }, - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Text(error.toString()), - ); + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: const Text("Streaming music quality"), + value: sourcePresets.selectedStreamingQualityIndex, + options: [ + for (final MapEntry(:key, value: quality) in sourcePresets + .presets[sourcePresets.selectedStreamingContainerIndex] + .qualities + .asMap() + .entries) + SelectItemButton(value: key, child: Text(quality.toString())), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingQualityIndex(value); }, ), - ), - switch (preferences.audioSource) { - AudioSource.youtube => AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.engine), - title: Text(context.l10n.youtube_engine), - value: preferences.youtubeClientEngine, - options: YoutubeClientEngine.values - .where((e) => e.isAvailableForPlatform()) - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) async { - if (value == null) return; - if (value == YoutubeClientEngine.ytDlp) { - final customPath = KVStoreService.getYoutubeEnginePath(value); - if (!await YtDlpEngine.isInstalled() && - (customPath == null || - !await File(customPath).exists()) && - context.mounted) { - final hasInstalled = await showDialog( - context: context, - builder: (context) => - YouTubeEngineNotInstalledDialog(engine: value), - ); - if (hasInstalled != true) return; - } - } - preferencesNotifier.setYoutubeClientEngine(value); - }, - ), - AudioSource.piped || - AudioSource.invidious => - AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.search), - title: Text(context.l10n.search_mode), - value: preferences.searchMode, - options: SearchMode.values - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setSearchMode(value); - }, - ), - _ => const SizedBox.shrink(), - }, - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.searchMode == SearchMode.youtube && - (preferences.audioSource == AudioSource.piped || - preferences.audioSource == AudioSource.youtube || - preferences.audioSource == AudioSource.invidious) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: ListTile( - leading: const Icon(SpotubeIcons.skip), - title: Text(context.l10n.skip_non_music), - trailing: Switch( - value: preferences.skipNonMusic, - onChanged: (state) { - preferencesNotifier.setSkipNonMusic(state); - }, - ), + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: Text(context.l10n.download_music_codec), + value: sourcePresets.selectedDownloadingContainerIndex, + options: [ + for (final MapEntry(:key, value: preset) + in sourcePresets.presets.asMap().entries) + SelectItemButton(value: key, child: Text(preset.name)), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedDownloadingContainerIndex(value); + }, ), - secondChild: const SizedBox.shrink(), - ), + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.api), + title: const Text("Downloading music quality"), + value: sourcePresets.selectedStreamingQualityIndex, + options: [ + for (final MapEntry(:key, value: quality) in sourcePresets + .presets[sourcePresets.selectedDownloadingContainerIndex] + .qualities + .asMap() + .entries) + SelectItemButton(value: key, child: Text(quality.toString())), + ], + onChanged: (value) { + if (value == null) return; + sourcePresetsNotifier.setSelectedStreamingQualityIndex(value); + }, + ), + ], ListTile( title: Text(context.l10n.cache_music), subtitle: kIsMobile @@ -402,50 +137,6 @@ class SettingsPlaybackSection extends HookConsumerWidget { onChanged: preferencesNotifier.setNormalizeAudio, ), ), - if (const [AudioSource.jiosaavn, AudioSource.dabMusic] - .contains(preferences.audioSource) == - false) ...[ - AdaptiveSelectTile( - popupConstraints: const BoxConstraints(maxWidth: 300), - secondary: const Icon(SpotubeIcons.stream), - title: Text(context.l10n.streaming_music_codec), - value: preferences.streamMusicCodec, - showValueWhenUnfolded: false, - options: SourceCodecs.values - .map((e) => SelectItemButton( - value: e, - child: Text( - e.label, - style: theme.typography.small, - ), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setStreamMusicCodec(value); - }, - ), - AdaptiveSelectTile( - popupConstraints: const BoxConstraints(maxWidth: 300), - secondary: const Icon(SpotubeIcons.file), - title: Text(context.l10n.download_music_codec), - value: preferences.downloadMusicCodec, - showValueWhenUnfolded: false, - options: SourceCodecs.values - .map((e) => SelectItemButton( - value: e, - child: Text( - e.label, - style: theme.typography.small, - ), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setDownloadMusicCodec(value); - }, - ), - ], ListTile( leading: const Icon(SpotubeIcons.repeat), title: Text(context.l10n.endless_playback), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 5db28125..2d569ab5 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -12,7 +12,7 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/discord_provider.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -164,8 +164,8 @@ class AudioPlayerNotifier extends Notifier { final tracks = []; for (final media in playlist.medias) { - final trackQuery = TrackSourceQuery.parseUri(media.uri); - final track = trackGroupedById[trackQuery.id]?.firstOrNull; + final track = trackGroupedById[SpotubeMedia.media(media).track.id] + ?.firstOrNull; if (track != null) { tracks.add(track); } @@ -401,9 +401,8 @@ class AudioPlayerNotifier extends Notifier { final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { await ref.read( - trackSourcesProvider( - TrackSourceQuery.fromTrack( - intendedActiveTrack.track as SpotubeFullTrackObject), + sourcedTrackProvider( + intendedActiveTrack.track as SpotubeFullTrackObject, ).future, ); } diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 507e9d49..eff13134 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -3,14 +3,13 @@ import 'dart:math'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/metadata_plugin/core/scrobble.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/provider/skip_segments/skip_segments.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -156,9 +155,7 @@ class AudioPlayerStreamListeners { try { await ref.read( - trackSourcesProvider( - TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject), - ).future, + sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future, ); } finally { lastTrack = nextTrack.id; diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index ce99b261..06e9653c 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -1,8 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; final queryingTrackInfoProvider = Provider((ref) { final audioPlayer = ref.watch(audioPlayerProvider); @@ -16,10 +15,9 @@ final queryingTrackInfoProvider = Provider((ref) { } return ref - .watch(trackSourcesProvider( - TrackSourceQuery.fromTrack( - audioPlayer.activeTrack! as SpotubeFullTrackObject, - ), - )) + .watch( + sourcedTrackProvider( + audioPlayer.activeTrack! as SpotubeFullTrackObject), + ) .isLoading; }); diff --git a/lib/provider/audio_player/sources/invidious_instances_provider.dart b/lib/provider/audio_player/sources/invidious_instances_provider.dart deleted file mode 100644 index c04ac765..00000000 --- a/lib/provider/audio_player/sources/invidious_instances_provider.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/services/sourced_track/sources/invidious.dart'; - -final invidiousInstancesProvider = FutureProvider((ref) async { - final invidious = ref.watch(invidiousProvider); - - final instances = await invidious.instances(); - - return instances - .where((instance) => instance.details.type == "https") - .toList(); -}); diff --git a/lib/provider/audio_player/sources/piped_instances_provider.dart b/lib/provider/audio_player/sources/piped_instances_provider.dart deleted file mode 100644 index 3c5d5f04..00000000 --- a/lib/provider/audio_player/sources/piped_instances_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:spotube/services/logger/logger.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/services/sourced_track/sources/piped.dart'; - -final pipedInstancesFutureProvider = FutureProvider>( - (ref) async { - try { - final pipedClient = ref.watch(pipedProvider); - - return await pipedClient.instanceList(); - } catch (e, stack) { - AppLogger.reportError(e, stack); - return []; - } - }, -); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index d0112765..bc1de813 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -26,7 +26,10 @@ class DownloadManagerProvider extends ChangeNotifier { final (:request, :status) = event; final sourcedTrack = $history.firstWhereOrNull( - (element) => element.getUrlOfCodec(downloadCodec) == request.url, + (element) => + element.getUrlOfQuality( + downloadContainer, downloadQualityIndex) == + request.url, ); if (sourcedTrack == null) return; final track = $backHistory.firstWhereOrNull( @@ -48,7 +51,8 @@ class DownloadManagerProvider extends ChangeNotifier { //? WebA audiotagging is not supported yet //? Although in future by converting weba to opus & then tagging it //? is possible using vorbis comments - downloadCodec == SourceCodecs.weba) { + downloadContainer.name == "weba" || + downloadContainer.name == "webm") { return; } @@ -87,8 +91,13 @@ class DownloadManagerProvider extends ChangeNotifier { String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); - SourceCodecs get downloadCodec => - ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec)); + SpotubeAudioSourceContainerPreset get downloadContainer => ref.read( + audioSourcePresetsProvider + .select((s) => s.presets[s.selectedDownloadingContainerIndex]), + ); + + int get downloadQualityIndex => ref.read(audioSourcePresetsProvider + .select((s) => s.selectedDownloadingQualityIndex)); int get $downloadCount => dl .getAllDownloads() @@ -107,7 +116,7 @@ class DownloadManagerProvider extends ChangeNotifier { String getTrackFileUrl(SourcedTrack track) { final name = - "${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}"; + "${track.query.name} - ${track.query.artists.join(", ")}.${downloadContainer.name}"; return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } @@ -129,13 +138,16 @@ class DownloadManagerProvider extends ChangeNotifier { download.status.value == DownloadStatus.queued, ) .map((e) => e.request.url) - .contains(sourcedTrack.getUrlOfCodec(downloadCodec)!); + .contains(sourcedTrack.getUrlOfQuality( + downloadContainer, + downloadQualityIndex, + )!); } /// For singular downloads Future addToQueue(SpotubeFullTrackObject track) async { final sourcedTrack = await ref.read( - trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, + sourcedTrackProvider(track).future, ); final savePath = getTrackFileUrl(sourcedTrack); @@ -149,9 +161,9 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.rename("$savePath.old"); } - if (sourcedTrack.codec == downloadCodec) { + if (sourcedTrack.qualityPreset == downloadContainer) { final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec)!, + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, savePath, ); if (downloadTask != null) { @@ -159,18 +171,13 @@ class DownloadManagerProvider extends ChangeNotifier { } } else { $backHistory.add(track); - final sourcedTrack = await ref - .read( - trackSourcesProvider( - TrackSourceQuery.fromTrack(track), - ).future, - ) - .then((d) { + final sourcedTrack = + await ref.read(sourcedTrackProvider(track).future).then((d) { $backHistory.remove(track); return d; }); final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfCodec(downloadCodec)!, + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, savePath, ); if (downloadTask != null) { @@ -203,18 +210,21 @@ class DownloadManagerProvider extends ChangeNotifier { Future removeFromQueue(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + await dl.removeDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); $history.remove(sourcedTrack); } Future pause(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.pauseDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } Future resume(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.resumeDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } Future retry(SpotubeFullTrackObject track) { @@ -223,7 +233,8 @@ class DownloadManagerProvider extends ChangeNotifier { void cancel(SpotubeFullTrackObject track) async { final sourcedTrack = await mapToSourcedTrack(track); - return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); + return dl.cancelDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); } void cancelAll() { @@ -241,9 +252,7 @@ class DownloadManagerProvider extends ChangeNotifier { return historicTrack; } - final sourcedTrack = await ref.read( - trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, - ); + final sourcedTrack = await ref.read(sourcedTrackProvider(track).future); return sourcedTrack; } @@ -257,7 +266,10 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.status; + return dl + .getDownload(sourcedTrack.getUrlOfQuality( + downloadContainer, downloadQualityIndex)!) + ?.status; } ValueNotifier? getProgressNotifier(SpotubeFullTrackObject track) { @@ -267,7 +279,10 @@ class DownloadManagerProvider extends ChangeNotifier { if (sourcedTrack == null) { return null; } - return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.progress; + return dl + .getDownload(sourcedTrack.getUrlOfQuality( + downloadContainer, downloadQualityIndex)!) + ?.progress; } } diff --git a/lib/provider/metadata_plugin/audio_source/quality_label.dart b/lib/provider/metadata_plugin/audio_source/quality_label.dart new file mode 100644 index 00000000..7d1dc95a --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_label.dart @@ -0,0 +1,12 @@ +import 'package:riverpod/riverpod.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; + +final audioSourceQualityLabelProvider = Provider((ref) { + final sourceQuality = ref.watch(audioSourcePresetsProvider); + final sourceContainer = + sourceQuality.presets[sourceQuality.selectedStreamingContainerIndex]; + final quality = + sourceContainer.qualities[sourceQuality.selectedStreamingQualityIndex]; + + return "${sourceContainer.name} • ${quality.toString()}"; +}); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart new file mode 100644 index 00000000..9cc7dc44 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +part 'quality_presets.g.dart'; +part 'quality_presets.freezed.dart'; + +@freezed +class AudioSourcePresetsState with _$AudioSourcePresetsState { + factory AudioSourcePresetsState({ + @Default([]) final List presets, + @Default(0) final int selectedStreamingQualityIndex, + @Default(0) final int selectedStreamingContainerIndex, + @Default(0) final int selectedDownloadingQualityIndex, + @Default(0) final int selectedDownloadingContainerIndex, + }) = _AudioSourcePresetsState; + + factory AudioSourcePresetsState.fromJson(Map json) => + _$AudioSourcePresetsStateFromJson(json); +} + +class AudioSourceAvailableQualityPresetsNotifier + extends Notifier { + @override + build() { + ref.watch(audioSourcePluginProvider); + + _initialize(); + + listenSelf((previous, next) { + final isNewLossless = + next.presets.elementAtOrNull(next.selectedStreamingContainerIndex) + is SpotubeAudioSourceContainerPresetLossless; + final isOldLossless = previous?.presets + .elementAtOrNull(previous.selectedStreamingContainerIndex) + is SpotubeAudioSourceContainerPresetLossless; + if (!isOldLossless && isNewLossless) { + audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB + } else if (isOldLossless && !isNewLossless) { + audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB + } + }); + + return AudioSourcePresetsState(); + } + + void _initialize() async { + final audioSource = await ref.read(audioSourcePluginProvider.future); + final audioSourceConfig = await ref.read( + metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig), + ); + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + final preferences = await SharedPreferences.getInstance(); + final persistedStateStr = + preferences.getString("audioSourceState-${audioSourceConfig.slug}"); + + if (persistedStateStr != null) { + state = AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + } else { + state = AudioSourcePresetsState( + presets: audioSource.audioSource.supportedPresets, + ); + } + } + + void setSelectedStreamingContainerIndex(int index) { + state = state.copyWith( + selectedStreamingContainerIndex: index, + selectedStreamingQualityIndex: + 0, // Resetting both because it's a different quality + ); + _updatePreferences(); + } + + void setSelectedStreamingQualityIndex(int index) { + state = state.copyWith(selectedStreamingQualityIndex: index); + _updatePreferences(); + } + + void setSelectedDownloadingContainerIndex(int index) { + state = state.copyWith( + selectedDownloadingContainerIndex: index, + selectedDownloadingQualityIndex: + 0, // Resetting both because it's a different quality + ); + _updatePreferences(); + } + + void setSelectedDownloadingQualityIndex(int index) { + state = state.copyWith(selectedDownloadingQualityIndex: index); + _updatePreferences(); + } + + void _updatePreferences() async { + final audioSourceConfig = await ref.read(metadataPluginsProvider + .selectAsync((data) => data.defaultAudioSourcePluginConfig)); + if (audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + + final preferences = await SharedPreferences.getInstance(); + await preferences.setString( + "audioSourceState-${audioSourceConfig.slug}", + jsonEncode(state), + ); + } +} + +final audioSourcePresetsProvider = NotifierProvider< + AudioSourceAvailableQualityPresetsNotifier, AudioSourcePresetsState>( + () => AudioSourceAvailableQualityPresetsNotifier(), +); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart new file mode 100644 index 00000000..a8e0c9f7 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.freezed.dart @@ -0,0 +1,289 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'quality_presets.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +AudioSourcePresetsState _$AudioSourcePresetsStateFromJson( + Map json) { + return _AudioSourcePresetsState.fromJson(json); +} + +/// @nodoc +mixin _$AudioSourcePresetsState { + List get presets => + throw _privateConstructorUsedError; + int get selectedStreamingQualityIndex => throw _privateConstructorUsedError; + int get selectedStreamingContainerIndex => throw _privateConstructorUsedError; + int get selectedDownloadingQualityIndex => throw _privateConstructorUsedError; + int get selectedDownloadingContainerIndex => + throw _privateConstructorUsedError; + + /// Serializes this AudioSourcePresetsState to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AudioSourcePresetsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AudioSourcePresetsStateCopyWith<$Res> { + factory $AudioSourcePresetsStateCopyWith(AudioSourcePresetsState value, + $Res Function(AudioSourcePresetsState) then) = + _$AudioSourcePresetsStateCopyWithImpl<$Res, AudioSourcePresetsState>; + @useResult + $Res call( + {List presets, + int selectedStreamingQualityIndex, + int selectedStreamingContainerIndex, + int selectedDownloadingQualityIndex, + int selectedDownloadingContainerIndex}); +} + +/// @nodoc +class _$AudioSourcePresetsStateCopyWithImpl<$Res, + $Val extends AudioSourcePresetsState> + implements $AudioSourcePresetsStateCopyWith<$Res> { + _$AudioSourcePresetsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presets = null, + Object? selectedStreamingQualityIndex = null, + Object? selectedStreamingContainerIndex = null, + Object? selectedDownloadingQualityIndex = null, + Object? selectedDownloadingContainerIndex = null, + }) { + return _then(_value.copyWith( + presets: null == presets + ? _value.presets + : presets // ignore: cast_nullable_to_non_nullable + as List, + selectedStreamingQualityIndex: null == selectedStreamingQualityIndex + ? _value.selectedStreamingQualityIndex + : selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedStreamingContainerIndex: null == selectedStreamingContainerIndex + ? _value.selectedStreamingContainerIndex + : selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex + ? _value.selectedDownloadingQualityIndex + : selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingContainerIndex: null == + selectedDownloadingContainerIndex + ? _value.selectedDownloadingContainerIndex + : selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AudioSourcePresetsStateImplCopyWith<$Res> + implements $AudioSourcePresetsStateCopyWith<$Res> { + factory _$$AudioSourcePresetsStateImplCopyWith( + _$AudioSourcePresetsStateImpl value, + $Res Function(_$AudioSourcePresetsStateImpl) then) = + __$$AudioSourcePresetsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List presets, + int selectedStreamingQualityIndex, + int selectedStreamingContainerIndex, + int selectedDownloadingQualityIndex, + int selectedDownloadingContainerIndex}); +} + +/// @nodoc +class __$$AudioSourcePresetsStateImplCopyWithImpl<$Res> + extends _$AudioSourcePresetsStateCopyWithImpl<$Res, + _$AudioSourcePresetsStateImpl> + implements _$$AudioSourcePresetsStateImplCopyWith<$Res> { + __$$AudioSourcePresetsStateImplCopyWithImpl( + _$AudioSourcePresetsStateImpl _value, + $Res Function(_$AudioSourcePresetsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presets = null, + Object? selectedStreamingQualityIndex = null, + Object? selectedStreamingContainerIndex = null, + Object? selectedDownloadingQualityIndex = null, + Object? selectedDownloadingContainerIndex = null, + }) { + return _then(_$AudioSourcePresetsStateImpl( + presets: null == presets + ? _value._presets + : presets // ignore: cast_nullable_to_non_nullable + as List, + selectedStreamingQualityIndex: null == selectedStreamingQualityIndex + ? _value.selectedStreamingQualityIndex + : selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedStreamingContainerIndex: null == selectedStreamingContainerIndex + ? _value.selectedStreamingContainerIndex + : selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex + ? _value.selectedDownloadingQualityIndex + : selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable + as int, + selectedDownloadingContainerIndex: null == + selectedDownloadingContainerIndex + ? _value.selectedDownloadingContainerIndex + : selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioSourcePresetsStateImpl implements _AudioSourcePresetsState { + _$AudioSourcePresetsStateImpl( + {final List presets = const [], + this.selectedStreamingQualityIndex = 0, + this.selectedStreamingContainerIndex = 0, + this.selectedDownloadingQualityIndex = 0, + this.selectedDownloadingContainerIndex = 0}) + : _presets = presets; + + factory _$AudioSourcePresetsStateImpl.fromJson(Map json) => + _$$AudioSourcePresetsStateImplFromJson(json); + + final List _presets; + @override + @JsonKey() + List get presets { + if (_presets is EqualUnmodifiableListView) return _presets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_presets); + } + + @override + @JsonKey() + final int selectedStreamingQualityIndex; + @override + @JsonKey() + final int selectedStreamingContainerIndex; + @override + @JsonKey() + final int selectedDownloadingQualityIndex; + @override + @JsonKey() + final int selectedDownloadingContainerIndex; + + @override + String toString() { + return 'AudioSourcePresetsState(presets: $presets, selectedStreamingQualityIndex: $selectedStreamingQualityIndex, selectedStreamingContainerIndex: $selectedStreamingContainerIndex, selectedDownloadingQualityIndex: $selectedDownloadingQualityIndex, selectedDownloadingContainerIndex: $selectedDownloadingContainerIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AudioSourcePresetsStateImpl && + const DeepCollectionEquality().equals(other._presets, _presets) && + (identical(other.selectedStreamingQualityIndex, + selectedStreamingQualityIndex) || + other.selectedStreamingQualityIndex == + selectedStreamingQualityIndex) && + (identical(other.selectedStreamingContainerIndex, + selectedStreamingContainerIndex) || + other.selectedStreamingContainerIndex == + selectedStreamingContainerIndex) && + (identical(other.selectedDownloadingQualityIndex, + selectedDownloadingQualityIndex) || + other.selectedDownloadingQualityIndex == + selectedDownloadingQualityIndex) && + (identical(other.selectedDownloadingContainerIndex, + selectedDownloadingContainerIndex) || + other.selectedDownloadingContainerIndex == + selectedDownloadingContainerIndex)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_presets), + selectedStreamingQualityIndex, + selectedStreamingContainerIndex, + selectedDownloadingQualityIndex, + selectedDownloadingContainerIndex); + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl> + get copyWith => __$$AudioSourcePresetsStateImplCopyWithImpl< + _$AudioSourcePresetsStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AudioSourcePresetsStateImplToJson( + this, + ); + } +} + +abstract class _AudioSourcePresetsState implements AudioSourcePresetsState { + factory _AudioSourcePresetsState( + {final List presets, + final int selectedStreamingQualityIndex, + final int selectedStreamingContainerIndex, + final int selectedDownloadingQualityIndex, + final int selectedDownloadingContainerIndex}) = + _$AudioSourcePresetsStateImpl; + + factory _AudioSourcePresetsState.fromJson(Map json) = + _$AudioSourcePresetsStateImpl.fromJson; + + @override + List get presets; + @override + int get selectedStreamingQualityIndex; + @override + int get selectedStreamingContainerIndex; + @override + int get selectedDownloadingQualityIndex; + @override + int get selectedDownloadingContainerIndex; + + /// Create a copy of AudioSourcePresetsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart new file mode 100644 index 00000000..f3d8fd41 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quality_presets.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AudioSourcePresetsStateImpl _$$AudioSourcePresetsStateImplFromJson( + Map json) => + _$AudioSourcePresetsStateImpl( + presets: (json['presets'] as List?) + ?.map((e) => SpotubeAudioSourceContainerPreset.fromJson( + Map.from(e as Map))) + .toList() ?? + const [], + selectedStreamingQualityIndex: + (json['selectedStreamingQualityIndex'] as num?)?.toInt() ?? 0, + selectedStreamingContainerIndex: + (json['selectedStreamingContainerIndex'] as num?)?.toInt() ?? 0, + selectedDownloadingQualityIndex: + (json['selectedDownloadingQualityIndex'] as num?)?.toInt() ?? 0, + selectedDownloadingContainerIndex: + (json['selectedDownloadingContainerIndex'] as num?)?.toInt() ?? 0, + ); + +Map _$$AudioSourcePresetsStateImplToJson( + _$AudioSourcePresetsStateImpl instance) => + { + 'presets': instance.presets.map((e) => e.toJson()).toList(), + 'selectedStreamingQualityIndex': instance.selectedStreamingQualityIndex, + 'selectedStreamingContainerIndex': + instance.selectedStreamingContainerIndex, + 'selectedDownloadingQualityIndex': + instance.selectedDownloadingQualityIndex, + 'selectedDownloadingContainerIndex': + instance.selectedDownloadingContainerIndex, + }; diff --git a/lib/provider/server/active_track_sources.dart b/lib/provider/server/active_track_sources.dart index 5b64dc26..603ca0e4 100644 --- a/lib/provider/server/active_track_sources.dart +++ b/lib/provider/server/active_track_sources.dart @@ -1,14 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final activeTrackSourcesProvider = FutureProvider< ({ SourcedTrack? source, - TrackSourcesNotifier? notifier, + SourcedTrackNotifier? notifier, SpotubeTrackObject track, })?>((ref) async { final audioPlayerState = ref.watch(audioPlayerProvider); @@ -25,13 +24,15 @@ final activeTrackSourcesProvider = FutureProvider< ); } - final trackQuery = TrackSourceQuery.fromTrack( - audioPlayerState.activeTrack! as SpotubeFullTrackObject, + final sourcedTrack = await ref.watch( + sourcedTrackProvider( + audioPlayerState.activeTrack! as SpotubeFullTrackObject, + ).future, ); - - final sourcedTrack = await ref.watch(trackSourcesProvider(trackQuery).future); final sourcedTrackNotifier = ref.watch( - trackSourcesProvider(trackQuery).notifier, + sourcedTrackProvider( + audioPlayerState.activeTrack! as SpotubeFullTrackObject, + ).notifier, ); return ( diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 7155edca..ec3a98a1 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -11,12 +11,11 @@ import 'package:path/path.dart'; import 'package:shelf/shelf.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/parser/range_headers.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; -import 'package:spotube/provider/server/track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -49,26 +48,30 @@ class ServerPlaybackRoutes { return join( await UserPreferencesNotifier.getMusicCacheDir(), ServiceUtils.sanitizeFilename( - '${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}', + '${track.query.name} - ${track.query.artists.join(",")} (${track.info.id}).${track.qualityPreset!.name}', ), ); } Future _getSourcedTrack( - Request request, String trackId) async { + Request request, + String trackId, + ) async { final track = playlist.tracks.firstWhere((element) => element.id == trackId); final activeSourcedTrack = await ref.read(activeTrackSourcesProvider.future); + + final media = audioPlayer.playlist.medias + .firstWhere((e) => e.uri == request.requestedUri.toString()); + final spotubeMedia = + media is SpotubeMedia ? media : SpotubeMedia.media(media); final sourcedTrack = activeSourcedTrack?.track.id == track.id ? activeSourcedTrack?.source : await ref.read( - trackSourcesProvider( - //! Use [Request.requestedUri] as it contains full https url. - //! [Request.url] will exclude and starts relatively. (streams/... basically) - TrackSourceQuery.parseUri(request.requestedUri.toString()), - ).future, + sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject) + .future, ); return sourcedTrack; @@ -79,7 +82,7 @@ class ServerPlaybackRoutes { SourcedTrack track, ) async { AppLogger.log.i( - "HEAD request for track: ${track.query.title}\n" + "HEAD request for track: ${track.query.name}\n" "Headers: ${request.headers}", ); @@ -91,7 +94,7 @@ class ServerPlaybackRoutes { return dio_lib.Response( statusCode: 200, headers: Headers.fromMap({ - "content-type": ["audio/${track.codec.name}"], + "content-type": ["audio/${track.qualityPreset!.name}"], "content-length": ["$fileLength"], "accept-ranges": ["bytes"], "content-range": ["bytes 0-$fileLength/$fileLength"], @@ -102,7 +105,7 @@ class ServerPlaybackRoutes { String url = track.url ?? await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); @@ -128,7 +131,7 @@ class ServerPlaybackRoutes { Map headers, ) async { AppLogger.log.i( - "GET request for track: ${track.query.title}\n" + "GET request for track: ${track.query.name}\n" "Headers: ${request.headers}", ); @@ -142,7 +145,7 @@ class ServerPlaybackRoutes { response: dio_lib.Response( statusCode: 200, headers: Headers.fromMap({ - "content-type": ["audio/${track.codec.name}"], + "content-type": ["audio/${track.qualityPreset!.name}"], "content-length": ["$cachedFileLength"], "accept-ranges": ["bytes"], "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], @@ -157,7 +160,7 @@ class ServerPlaybackRoutes { String url = track.url ?? await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); @@ -179,7 +182,7 @@ class ServerPlaybackRoutes { AppLogger.reportError(e, stack); final sourcedTrack = await ref - .read(trackSourcesProvider(track.query).notifier) + .read(sourcedTrackProvider(track.query).notifier) .refreshStreamingUrl(); url = sourcedTrack.url!; @@ -205,11 +208,9 @@ class ServerPlaybackRoutes { ); } - if (headers["range"] == "bytes=0-" && track.codec == SourceCodecs.flac) { - final bufferSize = - userPreferences.audioQuality == SourceQualities.uncompressed - ? 6 * 1024 * 1024 // 6MB for lossless - : 4 * 1024 * 1024; // 4MB for lossy + if (headers["range"] == "bytes=0-" && + track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) { + const bufferSize = 6 * 1024 * 1024; // 6MB for lossless final endRange = min( bufferSize, @@ -227,7 +228,7 @@ class ServerPlaybackRoutes { final res = await dio.get(url, options: options); AppLogger.log.i( - "Response for track: ${track.query.title}\n" + "Response for track: ${track.query.name}\n" "Status Code: ${res.statusCode}\n" "Headers: ${res.headers.map}", ); @@ -261,7 +262,9 @@ class ServerPlaybackRoutes { await trackPartialCacheFile.rename(trackCacheFile.path); } - if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) { + if (contentRange.total == fileLength && + track.qualityPreset!.name != "webm" || + track.qualityPreset!.name != "weba") { final playlistTrack = playlist.tracks.firstWhereOrNull( (element) => element.id == track.query.id, ); diff --git a/lib/provider/server/sourced_track_provider.dart b/lib/provider/server/sourced_track_provider.dart new file mode 100644 index 00000000..7934ecc7 --- /dev/null +++ b/lib/provider/server/sourced_track_provider.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; +import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; + +class SourcedTrackNotifier + extends FamilyAsyncNotifier { + @override + FutureOr build(query) { + ref.watch(audioSourcePluginProvider); + ref.watch(audioSourcePresetsProvider); + + return SourcedTrack.fetchFromTrack(query: query, ref: ref); + } + + Future refreshStreamingUrl() async { + return await update((prev) async { + return await prev.refreshStream(); + }); + } + + Future copyWithSibling() async { + return await update((prev) async { + return prev.copyWithSibling(); + }); + } + + Future swapWithSibling( + SpotubeAudioSourceMatchObject sibling, + ) async { + return await update((prev) async { + return await prev.swapWithSibling(sibling) ?? prev; + }); + } + + Future swapWithNextSibling() async { + return await update((prev) async { + return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack; + }); + } +} + +final sourcedTrackProvider = AsyncNotifierProviderFamily( + () => SourcedTrackNotifier(), +); diff --git a/lib/provider/server/track_sources.dart b/lib/provider/server/track_sources.dart deleted file mode 100644 index 24502471..00000000 --- a/lib/provider/server/track_sources.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:async'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/playback/track_sources.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; - -class TrackSourcesNotifier - extends FamilyAsyncNotifier { - @override - FutureOr build(query) { - ref.watch(userPreferencesProvider.select((p) => p.audioQuality)); - ref.watch(userPreferencesProvider.select((p) => p.audioSource)); - ref.watch(userPreferencesProvider.select((p) => p.streamMusicCodec)); - ref.watch(userPreferencesProvider.select((p) => p.downloadMusicCodec)); - - return SourcedTrack.fetchFromQuery(query: query, ref: ref); - } - - Future refreshStreamingUrl() async { - return await update((prev) async { - return await prev.refreshStream(); - }); - } - - Future copyWithSibling() async { - return await update((prev) async { - return prev.copyWithSibling(); - }); - } - - Future swapWithSibling(TrackSourceInfo sibling) async { - return await update((prev) async { - return await prev.swapWithSibling(sibling) ?? prev; - }); - } - - Future swapWithNextSibling() async { - return await update((prev) async { - return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack; - }); - } -} - -final trackSourcesProvider = AsyncNotifierProviderFamily( - () => TrackSourcesNotifier(), -); diff --git a/lib/provider/skip_segments/skip_segments.dart b/lib/provider/skip_segments/skip_segments.dart index accccddd..dc06f326 100644 --- a/lib/provider/skip_segments/skip_segments.dart +++ b/lib/provider/skip_segments/skip_segments.dart @@ -86,18 +86,10 @@ final segmentProvider = FutureProvider( if (snapshot == null) return null; final (:track, :source, :notifier) = snapshot; if (track is SpotubeLocalTrackObject) return null; - if (source!.source case AudioSource.jiosaavn) return null; + if (!source!.source.toLowerCase().contains("youtube")) return null; - final skipNonMusic = ref.watch( - userPreferencesProvider.select( - (s) { - final isPipedYTMusicMode = s.audioSource == AudioSource.piped && - s.searchMode == SearchMode.youtubeMusic; - - return s.skipNonMusic && !isPipedYTMusicMode; - }, - ), - ); + final skipNonMusic = + ref.watch(userPreferencesProvider.select((s) => s.skipNonMusic)); if (!skipNonMusic) { return SourcedSegments(segments: [], source: source.info.id); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 9bc64f4f..0b43d043 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -53,7 +53,6 @@ class UserPreferencesNotifier extends Notifier { } await audioPlayer.setAudioNormalization(state.normalizeAudio); - await _updatePlayerBufferSize(event.audioQuality, state.audioQuality); } catch (e, stack) { AppLogger.reportError(e, stack); } @@ -79,24 +78,6 @@ class UserPreferencesNotifier extends Notifier { }); } - /// Sets audio player's buffer size based on the selected audio quality - /// Uncompressed quality gets a larger buffer size for smoother playback - /// while other qualities use a standard buffer size. - Future _updatePlayerBufferSize( - SourceQualities newQuality, - SourceQualities oldQuality, - ) async { - if (newQuality == SourceQualities.uncompressed) { - audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB - return; - } - - if (oldQuality == SourceQualities.uncompressed && - newQuality != SourceQualities.uncompressed) { - audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB - } - } - Future setData(PreferencesTableCompanion data) async { final db = ref.read(databaseProvider); @@ -137,14 +118,6 @@ class UserPreferencesNotifier extends Notifier { } } - void setStreamMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); - } - - void setDownloadMusicCodec(SourceCodecs codec) { - setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); - } - void setThemeMode(ThemeMode mode) { setData(PreferencesTableCompanion(themeMode: Value(mode))); } @@ -171,11 +144,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(checkUpdate: Value(check))); } - void setAudioQuality(SourceQualities quality) { - setData(PreferencesTableCompanion(audioQuality: Value(quality))); - _updatePlayerBufferSize(quality, state.audioQuality); - } - void setDownloadLocation(String downloadDir) { if (downloadDir.isEmpty) return; setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); @@ -206,14 +174,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(locale: Value(locale))); } - void setPipedInstance(String instance) { - setData(PreferencesTableCompanion(pipedInstance: Value(instance))); - } - - void setInvidiousInstance(String instance) { - setData(PreferencesTableCompanion(invidiousInstance: Value(instance))); - } - void setSearchMode(SearchMode mode) { setData(PreferencesTableCompanion(searchMode: Value(mode))); } @@ -222,27 +182,6 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); } - void setAudioSource(AudioSource type) { - switch ((type, state.audioQuality)) { - // DAB music only supports high quality/uncompressed streams - case ( - AudioSource.dabMusic, - SourceQualities.low || SourceQualities.medium - ): - setAudioQuality(SourceQualities.high); - break; - // If the user switches from DAB music to other sources and has - // uncompressed quality selected, downgrade to high quality - case (!= AudioSource.dabMusic, SourceQualities.uncompressed): - setAudioQuality(SourceQualities.high); - break; - default: - break; - } - - setData(PreferencesTableCompanion(audioSource: Value(type))); - } - void setYoutubeClientEngine(YoutubeClientEngine engine) { setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine))); } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 262b9d10..a30fafba 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; @@ -22,21 +21,9 @@ class SpotubeMedia extends mk.Media { static String get _host => kIsWindows ? "localhost" : InternetAddress.anyIPv4.address; - static String _queries(SpotubeFullTrackObject track) { - final params = TrackSourceQuery.fromTrack(track).toJson(); - - return params.entries - .map((e) => - "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List ? e.value.join(",") : e.value.toString())}") - .join("&"); - } - final SpotubeTrackObject track; - SpotubeMedia( - this.track, { - Map? extras, - super.httpHeaders, - }) : assert( + SpotubeMedia(this.track) + : assert( track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject, "Track must be a either a local track or a full track object with ISRC", ), @@ -44,8 +31,14 @@ class SpotubeMedia extends mk.Media { super( track is SpotubeLocalTrackObject ? track.path - : "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}", + : "http://$_host:$serverPort/stream/${track.id}", + extras: track.toJson(), ); + + factory SpotubeMedia.media(Media media) { + assert(media.extras != null, "[Media] must have extra metadata set"); + return SpotubeMedia(SpotubeFullTrackObject.fromJson(media.extras!)); + } } abstract class AudioPlayerInterface { diff --git a/lib/services/sourced_track/exceptions.dart b/lib/services/sourced_track/exceptions.dart index c841e1e2..4817c9fb 100644 --- a/lib/services/sourced_track/exceptions.dart +++ b/lib/services/sourced_track/exceptions.dart @@ -1,12 +1,12 @@ -import 'package:spotube/models/playback/track_sources.dart'; +import 'package:spotube/models/metadata/metadata.dart'; class TrackNotFoundError extends Error { - final TrackSourceQuery track; + final SpotubeTrackObject track; TrackNotFoundError(this.track); @override String toString() { - return '[TrackNotFoundError] ${track.title} - ${track.artists.join(", ")}'; + return '[TrackNotFoundError] ${track.name} - ${track.artists.join(", ")}'; } } diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 661a8447..76b202da 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -4,13 +4,12 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/extensions/string.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -34,19 +33,7 @@ class SourcedTrack extends BasicSourcedTrack { required super.sources, }); - static String getSearchTerm(SpotubeFullTrackObject track) { - final title = ServiceUtils.getTitle( - track.name, - artists: track.artists.map((e) => e.name).toList(), - onlyCleanArtist: true, - ).trim(); - - assert(title.trim().isNotEmpty, "Title should not be empty"); - - return "$title - ${track.artists.join(", ")}"; - } - - static Future fetchFromQuery({ + static Future fetchFromTrack({ required SpotubeFullTrackObject query, required Ref ref, }) async { @@ -79,22 +66,25 @@ class SourcedTrack extends BasicSourcedTrack { await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: query.id, - source: jsonEncode(siblings.first), - sourceType: Value(audioSourceConfig.slug), + sourceInfo: Value(jsonEncode(siblings.first)), + sourceType: audioSourceConfig.slug, ), ); + final manifest = await audioSource.audioSource.streams(siblings.first); + return SourcedTrack( ref: ref, - siblings: siblings.map((s) => s.info).skip(1).toList(), - info: siblings.first.info, + siblings: siblings.skip(1).toList(), + info: siblings.first, source: audioSourceConfig.slug, - sources: siblings.first.source ?? [], + sources: manifest, query: query, ); } - final item = - SpotubeAudioSourceMatchObject.fromJson(jsonDecode(cachedSource.source)); + final item = SpotubeAudioSourceMatchObject.fromJson( + jsonDecode(cachedSource.sourceInfo), + ); final manifest = await audioSource.audioSource.streams(item); final sourcedTrack = SourcedTrack( @@ -229,8 +219,8 @@ class SourcedTrack extends BasicSourcedTrack { await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: query.id, - source: jsonEncode(siblings.first), - sourceType: Value(audioSourceConfig.slug), + sourceInfo: Value(jsonEncode(siblings.first)), + sourceType: audioSourceConfig.slug, createdAt: Value(DateTime.now()), ), mode: InsertMode.replace, @@ -298,13 +288,12 @@ class SourcedTrack extends BasicSourcedTrack { } String? get url { - final preferences = ref.read(userPreferencesProvider); + final preferences = ref.read(audioSourcePresetsProvider); - final codec = preferences.audioSource == AudioSource.jiosaavn - ? SourceCodecs.m4a - : preferences.streamMusicCodec; - - return getUrlOfCodec(codec); + return getUrlOfQuality( + preferences.presets[preferences.selectedStreamingContainerIndex], + preferences.selectedStreamingQualityIndex, + ); } /// Returns the URL of the track based on the codec and quality preferences. @@ -384,4 +373,10 @@ class SourcedTrack extends BasicSourcedTrack { ) { return getStreamOfQuality(preset, qualityIndex)?.url; } + + SpotubeAudioSourceContainerPreset? get qualityPreset { + final presetState = ref.read(audioSourcePresetsProvider); + return presetState.presets + .elementAtOrNull(presetState.selectedStreamingContainerIndex); + } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 81c4bfe4..738e4033 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -10,11 +10,9 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'; import 'package:spotube/modules/root/update_dialog.dart'; -import 'package:spotube/models/lyrics.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:collection/collection.dart'; @@ -189,95 +187,6 @@ abstract class ServiceUtils { return lyrics; } - @Deprecated("In favor spotify lyrics api, this isn't needed anymore") - static const baseUri = "https://www.rentanadviser.com/subtitles"; - - @Deprecated("In favor spotify lyrics api, this isn't needed anymore") - static Future getTimedLyrics(SourcedTrack track) async { - final artistNames = track.query.artists; - final query = getTitle( - track.query.title, - artists: artistNames, - ); - - final searchUri = Uri.parse("$baseUri/subtitles4songs.aspx").replace( - queryParameters: {"q": query}, - ); - - final res = await globalDio.getUri( - searchUri, - options: Options(responseType: ResponseType.plain), - ); - final document = parser.parse(res.data); - final results = - document.querySelectorAll("#tablecontainer table tbody tr td a"); - - final rateSortedResults = results.map((result) { - final title = result.text.trim().toLowerCase(); - int points = 0; - final hasAllArtists = track.query.artists - .every((artist) => title.contains(artist.toLowerCase())); - final hasTrackName = title.contains(track.query.title.toLowerCase()); - final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live"); - final exactYtMatch = title == track.info.title.toLowerCase(); - if (exactYtMatch) points = 7; - for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) { - if (criteria) points++; - } - return {"result": result, "points": points}; - }).sorted((a, b) => (b["points"] as int).compareTo(a["points"] as int)); - - // not result was found at all - if (rateSortedResults.first["points"] == 0) { - return Future.error("Subtitle lookup failed", StackTrace.current); - } - - final topResult = rateSortedResults.first["result"] as Element; - final subtitleUri = - Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc"); - - final lrcDocument = parser.parse((await globalDio.getUri( - subtitleUri, - options: Options(responseType: ResponseType.plain), - )) - .data); - final lrcList = lrcDocument - .querySelector("#ctl00_ContentPlaceHolder1_lbllyrics") - ?.innerHtml - .replaceAll(RegExp(r'

.*

'), "") - .split("
") - .map((e) { - e = e.trim(); - final regexp = RegExp(r'\[.*\]'); - final timeStr = regexp - .firstMatch(e) - ?.group(0) - ?.replaceAll(RegExp(r'\[|\]'), "") - .trim() - .split(":"); - final minuteSeconds = timeStr?.last.split("."); - - return LyricSlice( - time: Duration( - minutes: int.parse(timeStr?.first ?? "0"), - seconds: int.parse(minuteSeconds?.first ?? "0"), - milliseconds: int.parse(minuteSeconds?.last ?? "0"), - ), - text: e.split(regexp).last); - }).toList() ?? - []; - - final subtitle = SubtitleSimple( - name: topResult.text.trim(), - uri: subtitleUri, - lyrics: lrcList, - rating: rateSortedResults.first["points"] as int, - provider: "Rent An Adviser", - ); - - return subtitle; - } - static DateTime parseSpotifyAlbumDate(SpotubeFullAlbumObject? album) { if (album == null) { return DateTime.parse("1975-01-01"); diff --git a/pubspec.lock b/pubspec.lock index 8623af4e..0ae02b4c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -467,14 +467,6 @@ packages: url: "https://github.com/KRTirtho/dab_music_api.git" source: git version: "0.1.0" - dart_des: - dependency: transitive - description: - name: dart_des - sha256: "0a66afb8883368c824497fd2a1fd67bdb1a785965a3956728382c03d40747c33" - url: "https://pub.dev" - source: hosted - version: "1.0.2" dart_mappable: dependency: transitive description: @@ -1408,14 +1400,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - invidious: - dependency: "direct main" - description: - name: invidious - sha256: "0da8ebc4c4110057f03302bbd54514b10642154d7be569e7994172f2202dcfe8" - url: "https://pub.dev" - source: hosted - version: "0.1.2" io: dependency: "direct dev" description: @@ -1440,14 +1424,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - jiosaavn: - dependency: "direct main" - description: - name: jiosaavn - sha256: b6bde15c56398ebfd439825a64fb540a265773d1a518ba103e79988e13d16e1d - url: "https://pub.dev" - source: hosted - version: "0.1.1" jovial_misc: dependency: transitive description: @@ -1935,14 +1911,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.4" - piped_client: - dependency: "direct main" - description: - name: piped_client - sha256: "947613e2a8d368b72cb36473de2c5c2784e4e72b2d3f17e5a5181b98b1a5436e" - url: "https://pub.dev" - source: hosted - version: "0.1.2" pixel_snap: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4087bc0d..812e690f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,8 +81,6 @@ dependencies: http: ^1.2.1 image_picker: ^1.1.0 intl: any - invidious: ^0.1.2 - jiosaavn: ^0.1.1 json_annotation: ^4.8.1 local_notifier: ^0.1.6 logger: ^2.0.2 @@ -104,7 +102,6 @@ dependencies: path: ^1.9.0 path_provider: ^2.1.3 permission_handler: ^11.3.1 - piped_client: ^0.1.2 riverpod: ^2.5.1 scrobblenaut: git: diff --git a/test/drift/app_db/generated/schema_v10.dart b/test/drift/app_db/generated/schema_v10.dart index 36cc2a6b..2811ad02 100644 --- a/test/drift/app_db/generated/schema_v10.dart +++ b/test/drift/app_db/generated/schema_v10.dart @@ -552,16 +552,6 @@ class PreferencesTable extends Table type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const Constant("")); - late final GeneratedColumn pipedInstance = GeneratedColumn( - 'piped_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://pipedapi.kavin.rocks")); - late final GeneratedColumn invidiousInstance = - GeneratedColumn('invidious_instance', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant("https://inv.nadeko.net")); late final GeneratedColumn themeMode = GeneratedColumn( 'theme_mode', aliasedName, false, type: DriftSqlType.string, @@ -626,8 +616,6 @@ class PreferencesTable extends Table searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, audioSourceId, youtubeClientEngine, @@ -681,10 +669,6 @@ class PreferencesTable extends Table localLibraryLocation: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}local_library_location'])!, - pipedInstance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, - invidiousInstance: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}invidious_instance'])!, themeMode: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}theme_mode'])!, audioSourceId: attachedDatabase.typeMapping @@ -729,8 +713,6 @@ class PreferencesTableData extends DataClass final String searchMode; final String downloadLocation; final String localLibraryLocation; - final String pipedInstance; - final String invidiousInstance; final String themeMode; final String? audioSourceId; final String youtubeClientEngine; @@ -756,8 +738,6 @@ class PreferencesTableData extends DataClass required this.searchMode, required this.downloadLocation, required this.localLibraryLocation, - required this.pipedInstance, - required this.invidiousInstance, required this.themeMode, this.audioSourceId, required this.youtubeClientEngine, @@ -785,8 +765,6 @@ class PreferencesTableData extends DataClass map['search_mode'] = Variable(searchMode); map['download_location'] = Variable(downloadLocation); map['local_library_location'] = Variable(localLibraryLocation); - map['piped_instance'] = Variable(pipedInstance); - map['invidious_instance'] = Variable(invidiousInstance); map['theme_mode'] = Variable(themeMode); if (!nullToAbsent || audioSourceId != null) { map['audio_source_id'] = Variable(audioSourceId); @@ -818,8 +796,6 @@ class PreferencesTableData extends DataClass searchMode: Value(searchMode), downloadLocation: Value(downloadLocation), localLibraryLocation: Value(localLibraryLocation), - pipedInstance: Value(pipedInstance), - invidiousInstance: Value(invidiousInstance), themeMode: Value(themeMode), audioSourceId: audioSourceId == null && nullToAbsent ? const Value.absent() @@ -854,8 +830,6 @@ class PreferencesTableData extends DataClass downloadLocation: serializer.fromJson(json['downloadLocation']), localLibraryLocation: serializer.fromJson(json['localLibraryLocation']), - pipedInstance: serializer.fromJson(json['pipedInstance']), - invidiousInstance: serializer.fromJson(json['invidiousInstance']), themeMode: serializer.fromJson(json['themeMode']), audioSourceId: serializer.fromJson(json['audioSourceId']), youtubeClientEngine: @@ -887,8 +861,6 @@ class PreferencesTableData extends DataClass 'searchMode': serializer.toJson(searchMode), 'downloadLocation': serializer.toJson(downloadLocation), 'localLibraryLocation': serializer.toJson(localLibraryLocation), - 'pipedInstance': serializer.toJson(pipedInstance), - 'invidiousInstance': serializer.toJson(invidiousInstance), 'themeMode': serializer.toJson(themeMode), 'audioSourceId': serializer.toJson(audioSourceId), 'youtubeClientEngine': serializer.toJson(youtubeClientEngine), @@ -917,8 +889,6 @@ class PreferencesTableData extends DataClass String? searchMode, String? downloadLocation, String? localLibraryLocation, - String? pipedInstance, - String? invidiousInstance, String? themeMode, Value audioSourceId = const Value.absent(), String? youtubeClientEngine, @@ -944,8 +914,6 @@ class PreferencesTableData extends DataClass searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSourceId: audioSourceId.present ? audioSourceId.value : this.audioSourceId, @@ -997,12 +965,6 @@ class PreferencesTableData extends DataClass localLibraryLocation: data.localLibraryLocation.present ? data.localLibraryLocation.value : this.localLibraryLocation, - pipedInstance: data.pipedInstance.present - ? data.pipedInstance.value - : this.pipedInstance, - invidiousInstance: data.invidiousInstance.present - ? data.invidiousInstance.value - : this.invidiousInstance, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, audioSourceId: data.audioSourceId.present ? data.audioSourceId.value @@ -1045,8 +1007,6 @@ class PreferencesTableData extends DataClass ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') @@ -1077,8 +1037,6 @@ class PreferencesTableData extends DataClass searchMode, downloadLocation, localLibraryLocation, - pipedInstance, - invidiousInstance, themeMode, audioSourceId, youtubeClientEngine, @@ -1108,8 +1066,6 @@ class PreferencesTableData extends DataClass other.searchMode == this.searchMode && other.downloadLocation == this.downloadLocation && other.localLibraryLocation == this.localLibraryLocation && - other.pipedInstance == this.pipedInstance && - other.invidiousInstance == this.invidiousInstance && other.themeMode == this.themeMode && other.audioSourceId == this.audioSourceId && other.youtubeClientEngine == this.youtubeClientEngine && @@ -1137,8 +1093,6 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value searchMode; final Value downloadLocation; final Value localLibraryLocation; - final Value pipedInstance; - final Value invidiousInstance; final Value themeMode; final Value audioSourceId; final Value youtubeClientEngine; @@ -1164,8 +1118,6 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), @@ -1192,8 +1144,6 @@ class PreferencesTableCompanion extends UpdateCompanion { this.searchMode = const Value.absent(), this.downloadLocation = const Value.absent(), this.localLibraryLocation = const Value.absent(), - this.pipedInstance = const Value.absent(), - this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSourceId = const Value.absent(), this.youtubeClientEngine = const Value.absent(), @@ -1220,8 +1170,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Expression? searchMode, Expression? downloadLocation, Expression? localLibraryLocation, - Expression? pipedInstance, - Expression? invidiousInstance, Expression? themeMode, Expression? audioSourceId, Expression? youtubeClientEngine, @@ -1250,8 +1198,6 @@ class PreferencesTableCompanion extends UpdateCompanion { if (downloadLocation != null) 'download_location': downloadLocation, if (localLibraryLocation != null) 'local_library_location': localLibraryLocation, - if (pipedInstance != null) 'piped_instance': pipedInstance, - if (invidiousInstance != null) 'invidious_instance': invidiousInstance, if (themeMode != null) 'theme_mode': themeMode, if (audioSourceId != null) 'audio_source_id': audioSourceId, if (youtubeClientEngine != null) @@ -1281,8 +1227,6 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? searchMode, Value? downloadLocation, Value? localLibraryLocation, - Value? pipedInstance, - Value? invidiousInstance, Value? themeMode, Value? audioSourceId, Value? youtubeClientEngine, @@ -1308,8 +1252,6 @@ class PreferencesTableCompanion extends UpdateCompanion { searchMode: searchMode ?? this.searchMode, downloadLocation: downloadLocation ?? this.downloadLocation, localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, - pipedInstance: pipedInstance ?? this.pipedInstance, - invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSourceId: audioSourceId ?? this.audioSourceId, youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, @@ -1373,12 +1315,6 @@ class PreferencesTableCompanion extends UpdateCompanion { map['local_library_location'] = Variable(localLibraryLocation.value); } - if (pipedInstance.present) { - map['piped_instance'] = Variable(pipedInstance.value); - } - if (invidiousInstance.present) { - map['invidious_instance'] = Variable(invidiousInstance.value); - } if (themeMode.present) { map['theme_mode'] = Variable(themeMode.value); } @@ -1426,8 +1362,6 @@ class PreferencesTableCompanion extends UpdateCompanion { ..write('searchMode: $searchMode, ') ..write('downloadLocation: $downloadLocation, ') ..write('localLibraryLocation: $localLibraryLocation, ') - ..write('pipedInstance: $pipedInstance, ') - ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSourceId: $audioSourceId, ') ..write('youtubeClientEngine: $youtubeClientEngine, ') @@ -1935,7 +1869,9 @@ class SourceMatchTable extends Table type: DriftSqlType.string, requiredDuringInsert: true); late final GeneratedColumn sourceInfo = GeneratedColumn( 'source_info', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("{}")); late final GeneratedColumn sourceType = GeneratedColumn( 'source_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); @@ -2101,11 +2037,10 @@ class SourceMatchTableCompanion extends UpdateCompanion { SourceMatchTableCompanion.insert({ this.id = const Value.absent(), required String trackId, - required String sourceInfo, + this.sourceInfo = const Value.absent(), required String sourceType, this.createdAt = const Value.absent(), }) : trackId = Value(trackId), - sourceInfo = Value(sourceInfo), sourceType = Value(sourceType); static Insertable custom({ Expression? id, From 4b5108e54e4ff21dd561127e1d78d01578276174 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 3 Nov 2025 21:27:06 +0600 Subject: [PATCH 20/47] fix: streaming not working --- lib/models/database/database.dart | 28 +++++++++++---- lib/pages/settings/sections/playback.dart | 35 +++++++++++++++++++ lib/provider/audio_player/audio_player.dart | 3 +- .../audio_source/quality_label.dart | 10 +++--- .../metadata_plugin_provider.dart | 2 +- .../metadata/endpoints/audio_source.dart | 4 +-- pubspec.lock | 17 --------- pubspec.yaml | 4 --- 8 files changed, 65 insertions(+), 38 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 786b813f..55ff5abf 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,6 +19,7 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -211,13 +212,26 @@ class AppDatabase extends _$AppDatabase { ); }, from9To10: (m, schema) async { - await m.dropColumn(schema.preferencesTable, "piped_instance"); - await m.dropColumn(schema.preferencesTable, "invidious_instance"); - await m.addColumn( - schema.sourceMatchTable, - sourceMatchTable.sourceInfo, - ); - await m.dropColumn(schema.sourceMatchTable, "source_id"); + try { + await m + .dropColumn(schema.preferencesTable, "piped_instance") + .catchError((e) {}); + await m + .dropColumn(schema.preferencesTable, "invidious_instance") + .catchError((e) {}); + await m + .addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ) + .catchError((e) {}); + await m + .dropColumn(schema.sourceMatchTable, "source_id") + .catchError((e) {}); + } catch (e) { + AppLogger.log.e(e); + return; + } }, ), ); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 29a8c2ea..7a498ac0 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show ListTile; @@ -7,11 +9,15 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; @@ -30,6 +36,35 @@ class SettingsPlaybackSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.playback, children: [ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.engine), + title: Text(context.l10n.youtube_engine), + value: preferences.youtubeClientEngine, + options: YoutubeClientEngine.values + .where((e) => e.isAvailableForPlatform()) + .map((e) => SelectItemButton( + value: e, + child: Text(e.label), + )) + .toList(), + onChanged: (value) async { + if (value == null) return; + if (value == YoutubeClientEngine.ytDlp) { + final customPath = KVStoreService.getYoutubeEnginePath(value); + if (!await YtDlpEngine.isInstalled() && + (customPath == null || !await File(customPath).exists()) && + context.mounted) { + final hasInstalled = await showDialog( + context: context, + builder: (context) => + YouTubeEngineNotInstalledDialog(engine: value), + ); + if (hasInstalled != true) return; + } + } + preferencesNotifier.setYoutubeClientEngine(value); + }, + ), if (sourcePresets.presets.isNotEmpty) ...[ AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.api), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 2d569ab5..1bfd8f2d 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -7,7 +7,6 @@ import 'package:media_kit/media_kit.dart'; import 'package:spotube/extensions/list.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; -import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; @@ -400,7 +399,7 @@ class AudioPlayerNotifier extends Notifier { // because of timeout final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { - await ref.read( + ref.read( sourcedTrackProvider( intendedActiveTrack.track as SpotubeFullTrackObject, ).future, diff --git a/lib/provider/metadata_plugin/audio_source/quality_label.dart b/lib/provider/metadata_plugin/audio_source/quality_label.dart index 7d1dc95a..113ed54e 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_label.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_label.dart @@ -3,10 +3,10 @@ import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.da final audioSourceQualityLabelProvider = Provider((ref) { final sourceQuality = ref.watch(audioSourcePresetsProvider); - final sourceContainer = - sourceQuality.presets[sourceQuality.selectedStreamingContainerIndex]; - final quality = - sourceContainer.qualities[sourceQuality.selectedStreamingQualityIndex]; + final sourceContainer = sourceQuality.presets + .elementAtOrNull(sourceQuality.selectedStreamingContainerIndex); + final quality = sourceContainer?.qualities + .elementAtOrNull(sourceQuality.selectedStreamingQualityIndex); - return "${sourceContainer.name} • ${quality.toString()}"; + return "${sourceContainer?.name ?? "Unknown"} • ${quality?.toString() ?? "Unknown"}"; }); diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index 13d72c93..ab3c8547 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -543,7 +543,7 @@ final audioSourcePluginProvider = FutureProvider( metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig), ); - final youtubeEngine = ref.read(youtubeEngineProvider); + final youtubeEngine = ref.watch(youtubeEngineProvider); if (defaultPlugin == null) { return null; diff --git a/lib/services/metadata/endpoints/audio_source.dart b/lib/services/metadata/endpoints/audio_source.dart index 3493c112..d22449c6 100644 --- a/lib/services/metadata/endpoints/audio_source.dart +++ b/lib/services/metadata/endpoints/audio_source.dart @@ -22,7 +22,7 @@ class MetadataPluginAudioSourceEndpoint { SpotubeFullTrackObject track, ) async { final raw = await hetuMetadataAudioSource - .invoke("matches", positionalArgs: [track]) as List; + .invoke("matches", positionalArgs: [track.toJson()]) as List; return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList(); } @@ -31,7 +31,7 @@ class MetadataPluginAudioSourceEndpoint { SpotubeAudioSourceMatchObject match, ) async { final raw = await hetuMetadataAudioSource - .invoke("streams", positionalArgs: [match]) as List; + .invoke("streams", positionalArgs: [match.toJson()]) as List; return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList(); } diff --git a/pubspec.lock b/pubspec.lock index 0ae02b4c..6f004f11 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -458,15 +458,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - dab_music_api: - dependency: "direct main" - description: - path: "." - ref: main - resolved-ref: "55f96368b7465eec2e5e81774f9f2a7b18acc4ab" - url: "https://github.com/KRTirtho/dab_music_api.git" - source: git - version: "0.1.0" dart_mappable: dependency: transitive description: @@ -2023,14 +2014,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - retrofit: - dependency: transitive - description: - name: retrofit - sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25" - url: "https://pub.dev" - source: hosted - version: "4.7.2" riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 812e690f..9780e0fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,10 +24,6 @@ dependencies: bonsoir: ^5.1.10 cached_network_image: ^3.3.1 connectivity_plus: ^6.1.2 - dab_music_api: - git: - url: https://github.com/KRTirtho/dab_music_api.git - ref: main desktop_webview_window: git: path: packages/desktop_webview_window From 6272f376ea27b8af62548d17b3229d8001df41b8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 4 Nov 2025 12:02:10 +0600 Subject: [PATCH 21/47] fix: quality preset initialization fails and audio source auth --- android/.gitignore | 1 + lib/models/metadata/audio_source.dart | 2 +- .../metadata_plugins/installed_plugin.dart | 7 +-- .../audio_source/quality_presets.dart | 50 +++++++++++-------- lib/provider/metadata_plugin/core/auth.dart | 4 +- lib/provider/server/routes/playback.dart | 6 ++- lib/services/sourced_track/sourced_track.dart | 2 +- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/android/.gitignore b/android/.gitignore index 6f568019..2391a77e 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks +.kotlin \ No newline at end of file diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart index 44804285..898300e9 100644 --- a/lib/models/metadata/audio_source.dart +++ b/lib/models/metadata/audio_source.dart @@ -44,7 +44,7 @@ class SpotubeAudioLossyContainerQuality @override toString() { - return "${oneOptionalDecimalFormatter.format(bitrate)}kbps"; + return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps"; } } diff --git a/lib/modules/metadata_plugins/installed_plugin.dart b/lib/modules/metadata_plugins/installed_plugin.dart index 34881aaf..7abda5ec 100644 --- a/lib/modules/metadata_plugins/installed_plugin.dart +++ b/lib/modules/metadata_plugins/installed_plugin.dart @@ -52,9 +52,10 @@ class MetadataInstalledPluginItem extends HookConsumerWidget { ref.watch(metadataPluginAuthenticatedProvider); final isAudioSourceAuthenticatedSnapshot = ref.watch(audioSourcePluginAuthenticatedProvider); - final isAuthenticated = - isMetadataAuthenticatedSnapshot.asData?.value == true || - isAudioSourceAuthenticatedSnapshot.asData?.value == true; + final isAuthenticated = (isDefaultMetadata && + isMetadataAuthenticatedSnapshot.asData?.value == true) || + (isDefaultAudioSource && + isAudioSourceAuthenticatedSnapshot.asData?.value == true); final metadataUpdateAvailable = ref.watch(metadataPluginUpdateCheckerProvider); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart index 9cc7dc44..05028bc1 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_presets.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/metadata/metadata.dart'; part 'quality_presets.g.dart'; part 'quality_presets.freezed.dart'; @@ -28,9 +29,13 @@ class AudioSourceAvailableQualityPresetsNotifier extends Notifier { @override build() { - ref.watch(audioSourcePluginProvider); + final audioSourceSnapshot = ref.watch(audioSourcePluginProvider); + final audioSourceConfigSnapshot = ref.watch( + metadataPluginsProvider.select((data) => + data.whenData((value) => value.defaultAudioSourcePluginConfig)), + ); - _initialize(); + _initialize(audioSourceSnapshot, audioSourceConfigSnapshot); listenSelf((previous, next) { final isNewLossless = @@ -49,26 +54,29 @@ class AudioSourceAvailableQualityPresetsNotifier return AudioSourcePresetsState(); } - void _initialize() async { - final audioSource = await ref.read(audioSourcePluginProvider.future); - final audioSourceConfig = await ref.read( - metadataPluginsProvider - .selectAsync((data) => data.defaultAudioSourcePluginConfig), - ); - if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); - } - final preferences = await SharedPreferences.getInstance(); - final persistedStateStr = - preferences.getString("audioSourceState-${audioSourceConfig.slug}"); + void _initialize( + AsyncValue audioSourceSnapshot, + AsyncValue audioSourceConfigSnapshot, + ) async { + audioSourceConfigSnapshot.whenData((audioSourceConfig) { + audioSourceSnapshot.whenData((audioSource) async { + if (audioSource == null || audioSourceConfig == null) { + throw Exception("Dude wat?"); + } + final preferences = await SharedPreferences.getInstance(); + final persistedStateStr = + preferences.getString("audioSourceState-${audioSourceConfig.slug}"); - if (persistedStateStr != null) { - state = AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); - } else { - state = AudioSourcePresetsState( - presets: audioSource.audioSource.supportedPresets, - ); - } + if (persistedStateStr != null) { + state = + AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + } else { + state = AudioSourcePresetsState( + presets: audioSource.audioSource.supportedPresets, + ); + } + }); + }); } void setSelectedStreamingContainerIndex(int index) { diff --git a/lib/provider/metadata_plugin/core/auth.dart b/lib/provider/metadata_plugin/core/auth.dart index 647b94f9..dc5e7eb6 100644 --- a/lib/provider/metadata_plugin/core/auth.dart +++ b/lib/provider/metadata_plugin/core/auth.dart @@ -65,6 +65,6 @@ class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier { } final audioSourcePluginAuthenticatedProvider = - AsyncNotifierProvider( - MetadataPluginAuthenticatedNotifier.new, + AsyncNotifierProvider( + AudioSourcePluginAuthenticatedNotifier.new, ); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index ec3a98a1..792d7797 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -48,7 +48,7 @@ class ServerPlaybackRoutes { return join( await UserPreferencesNotifier.getMusicCacheDir(), ServiceUtils.sanitizeFilename( - '${track.query.name} - ${track.query.artists.join(",")} (${track.info.id}).${track.qualityPreset!.name}', + '${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.name}', ), ); } @@ -288,7 +288,9 @@ class ServerPlaybackRoutes { imageBytes: imageBytes, fileLength: fileLength, ), - ); + ).catchError((e, stackTrace) { + AppLogger.reportError(e, stackTrace); + }); } return (bytes: bytes, response: res); diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 76b202da..a738ffba 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -317,7 +317,7 @@ class SourcedTrack extends BasicSourcedTrack { source.bitDepth == quality.bitDepth; } else { return source.bitrate == - (preset as SpotubeAudioLossyContainerQuality).bitrate; + (quality as SpotubeAudioLossyContainerQuality).bitrate; } }, ); From e1fa9efa14ef6a95d55a57089e9fccad1bb90d61 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 4 Nov 2025 13:45:23 +0600 Subject: [PATCH 22/47] fix: selection preset quality returning null --- .../audio_source/quality_presets.dart | 5 +- lib/services/sourced_track/sourced_track.dart | 55 ++++++------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart index 05028bc1..0a8b00fe 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_presets.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -69,7 +69,10 @@ class AudioSourceAvailableQualityPresetsNotifier if (persistedStateStr != null) { state = - AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)); + AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr)) + .copyWith( + presets: audioSource.audioSource.supportedPresets, + ); } else { state = AudioSourcePresetsState( presets: audioSource.audioSource.supportedPresets, diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index a738ffba..5da54fc8 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -306,6 +306,8 @@ class SourcedTrack extends BasicSourcedTrack { SpotubeAudioSourceContainerPreset preset, int qualityIndex, ) { + if (sources.isEmpty) return null; + final quality = preset.qualities[qualityIndex]; final exactMatch = sources.firstWhereOrNull( @@ -326,45 +328,24 @@ class SourcedTrack extends BasicSourcedTrack { return exactMatch; } - // Find the closest to preset - SpotubeAudioSourceStreamObject? closest; - for (final source in sources) { - if (source.container != preset.name) continue; - - if (quality case SpotubeAudioLosslessContainerQuality()) { - final sourceBps = (source.bitDepth ?? 0) * (source.sampleRate ?? 0); - final qualityBps = quality.bitDepth * quality.sampleRate; - final closestBps = - (closest?.bitDepth ?? 0) * (closest?.sampleRate ?? 0); - - if (sourceBps == qualityBps) { - closest = source; - break; - } - final closestDiff = (closestBps - qualityBps).abs(); - final sourceDiff = (sourceBps - qualityBps).abs(); - - if (sourceDiff < closestDiff) { - closest = source; - } + // Find the preset with closest quality to the supplied quality + return sources.where((source) { + return source.container == preset.name; + }).reduce((prev, curr) { + if (quality is SpotubeAudioLosslessContainerQuality) { + final prevDiff = ((prev.sampleRate ?? 0) - quality.sampleRate).abs() + + ((prev.bitDepth ?? 0) - quality.bitDepth).abs(); + final currDiff = ((curr.sampleRate ?? 0) - quality.sampleRate).abs() + + ((curr.bitDepth ?? 0) - quality.bitDepth).abs(); + return currDiff < prevDiff ? curr : prev; } else { - final presetBitrate = - (preset as SpotubeAudioLossyContainerQuality).bitrate; - if (presetBitrate == source.bitrate) { - closest = source; - break; - } - - final closestDiff = (closest?.bitrate ?? 0) - presetBitrate; - final sourceDiff = (source.bitrate ?? 0) - presetBitrate; - - if (sourceDiff < closestDiff) { - closest = source; - } + final prevDiff = ((prev.bitrate ?? 0) - + (quality as SpotubeAudioLossyContainerQuality).bitrate) + .abs(); + final currDiff = ((curr.bitrate ?? 0) - quality.bitrate).abs(); + return currDiff < prevDiff ? curr : prev; } - } - - return closest; + }); } String? getUrlOfQuality( From d1b73dbb1c6050ef928136a036ef53edad083e56 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 18:48:18 +0600 Subject: [PATCH 23/47] feat: add NewPipe support for desktop platforms --- lib/main.dart | 2 ++ lib/services/youtube_engine/newpipe_engine.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.lock | 2 +- windows/flutter/generated_plugin_registrant.cc | 3 +++ windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f29933e6..ecf7148d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,6 +83,8 @@ Future main(List rawArgs) async { // force High Refresh Rate on some Android devices (like One Plus) if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); + } + if (kIsAndroid || kIsDesktop) { await NewPipeExtractor.init(); } diff --git a/lib/services/youtube_engine/newpipe_engine.dart b/lib/services/youtube_engine/newpipe_engine.dart index ae451e22..d6445a19 100644 --- a/lib/services/youtube_engine/newpipe_engine.dart +++ b/lib/services/youtube_engine/newpipe_engine.dart @@ -6,7 +6,7 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:http_parser/http_parser.dart'; class NewPipeEngine implements YouTubeEngine { - static bool get isAvailableForPlatform => kIsAndroid; + static bool get isAvailableForPlatform => kIsAndroid || kIsDesktop; AudioOnlyStreamInfo _parseAudioStream(AudioStream stream, String videoId) { return AudioOnlyStreamInfo( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index eda2d021..63e83265 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_new_pipe_extractor_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterNewPipeExtractorPlugin"); + flutter_new_pipe_extractor_plugin_register_with_registrar(flutter_new_pipe_extractor_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b9ca593f..e5c8a845 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window file_selector_linux + flutter_new_pipe_extractor flutter_secure_storage_linux flutter_timezone gtk diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9385ed14..d211f518 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,7 @@ import device_info_plus import file_picker import file_selector_macos import flutter_inappwebview_macos +import flutter_new_pipe_extractor import flutter_secure_storage_macos import flutter_timezone import irondash_engine_context @@ -44,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterNewPipeExtractorPlugin.register(with: registry.registrar(forPlugin: "FlutterNewPipeExtractorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6f004f11..4e98e422 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -955,7 +955,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: d4d71545111c8ca6c91f0040091c42d74cce1762 + resolved-ref: "898fd4ebcef77f5177b08aa6f9b9047bd02c6b9b" url: "https://github.com/KRTirtho/flutter_new_pipe_extractor.git" source: git version: "0.1.0" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 87b34e37..ac2fd1e0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); + FlutterNewPipeExtractorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterNewPipeExtractorPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterTimezonePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 798e47c8..6e831cf5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window file_selector_windows flutter_inappwebview_windows + flutter_new_pipe_extractor flutter_secure_storage_windows flutter_timezone irondash_engine_context From 64f937bd1421692693ecfdbb4c7873c75962f61c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 18:59:55 +0600 Subject: [PATCH 24/47] chore: remove useless appbundle build --- .github/workflows/spotube-publish-binary.yml | 46 +++++++++--------- .github/workflows/spotube-release-binary.yml | 1 - cli/commands/build/android.dart | 51 -------------------- pubspec.lock | 2 +- pubspec.yaml | 1 - 5 files changed, 24 insertions(+), 77 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index f88e618c..e682dbdd 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -12,10 +12,10 @@ on: type: boolean default: true jobs: - description: Jobs to run (flathub,aur,winget,chocolatey,playstore) + description: Jobs to run (flathub,aur,winget,chocolatey) required: true type: string - default: "flathub,aur,winget,chocolatey,playstore" + default: "flathub,aur,winget,chocolatey" jobs: flathub: @@ -112,26 +112,26 @@ jobs: - name: Tagname (workflow dispatch) run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV - - uses: robinraju/release-downloader@main - with: - repository: KRTirtho/spotube - tag: v${{ env.TAG_NAME }} - tarBall: false - zipBall: false - out-file-path: dist - fileName: "Spotube-playstore-all-arch.aab" + # - uses: robinraju/release-downloader@main + # with: + # repository: KRTirtho/spotube + # tag: v${{ env.TAG_NAME }} + # tarBall: false + # zipBall: false + # out-file-path: dist + # fileName: "Spotube-playstore-all-arch.aab" - - name: Create service-account.json - run: | - echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json + # - name: Create service-account.json + # run: | + # echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json - - name: Upload Android Release to Play Store - if: ${{!inputs.dry_run}} - uses: r0adkll/upload-google-play@v1 - with: - serviceAccountJson: ./service-account.json - releaseFiles: ./dist/Spotube-playstore-all-arch.aab - packageName: oss.krtirtho.spotube - track: production - status: draft - releaseName: ${{ env.TAG_NAME }} + # - name: Upload Android Release to Play Store + # if: ${{!inputs.dry_run}} + # uses: r0adkll/upload-google-play@v1 + # with: + # serviceAccountJson: ./service-account.json + # releaseFiles: ./dist/Spotube-playstore-all-arch.aab + # packageName: oss.krtirtho.spotube + # track: production + # status: draft + # releaseName: ${{ env.TAG_NAME }} diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 4f2cff34..2ddd7a6a 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -49,7 +49,6 @@ jobs: arch: all files: | build/Spotube-android-all-arch.apk - build/Spotube-playstore-all-arch.aab - os: windows-latest platform: windows arch: x86 diff --git a/cli/commands/build/android.dart b/cli/commands/build/android.dart index 4216553a..b9edeb84 100644 --- a/cli/commands/build/android.dart +++ b/cli/commands/build/android.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:collection/collection.dart'; import 'package:path/path.dart'; -import 'package:xml/xml.dart'; import '../../core/env.dart'; import 'common.dart'; @@ -24,39 +22,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps { "flutter build apk --flavor ${CliEnv.channel.name}", ); - await dotEnvFile.writeAsString( - "\nENABLE_UPDATE_CHECK=0" - "\nHIDE_DONATIONS=1", - mode: FileMode.append, - ); - - final androidManifestFile = File( - join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml")); - - final androidManifestXml = - XmlDocument.parse(await androidManifestFile.readAsString()); - - final deletingElement = - androidManifestXml.findAllElements("meta-data").firstWhereOrNull( - (el) => - el.getAttribute("android:name") == - "com.google.android.gms.car.application", - ); - - deletingElement?.parent?.children.remove(deletingElement); - - await androidManifestFile.writeAsString( - androidManifestXml.toXmlString(pretty: true), - ); - - await shell.run( - """ - dart run build_runner clean - dart run build_runner build --delete-conflicting-outputs - flutter build appbundle --flavor ${CliEnv.channel.name} - """, - ); - final ogApkFile = File( join( "build", @@ -71,22 +36,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps { join(cwd.path, "build", "Spotube-android-all-arch.apk"), ); - final ogAppbundleFile = File( - join( - cwd.path, - "build", - "app", - "outputs", - "bundle", - "${CliEnv.channel.name}Release", - "app-${CliEnv.channel.name}-release.aab", - ), - ); - - await ogAppbundleFile.copy( - join(cwd.path, "build", "Spotube-playstore-all-arch.aab"), - ); - stdout.writeln("✅ Built Android Apk and Appbundle"); } } diff --git a/pubspec.lock b/pubspec.lock index 4e98e422..91f1c2eb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2821,7 +2821,7 @@ packages: source: hosted version: "1.1.0" xml: - dependency: "direct dev" + dependency: transitive description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 diff --git a/pubspec.yaml b/pubspec.yaml index 9780e0fe..0c31a0cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -177,7 +177,6 @@ dev_dependencies: process_run: ^0.14.2 pubspec_parse: ^1.3.0 pub_api_client: ^3.0.0 - xml: ^6.5.0 io: ^1.0.4 drift_dev: ^2.21.0 auto_route_generator: ^9.0.0 From a012a8f3af9b144fa4048bb65f5cfb14e72fbc1a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 20:28:09 +0600 Subject: [PATCH 25/47] chore: fix unique index on source_match_table causing failure on insert --- lib/models/database/database.dart | 29 ++++++-------------- lib/models/database/tables/source_match.dart | 5 ---- pubspec.lock | 4 +-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 55ff5abf..387bcdb7 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,7 +19,6 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; -import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -212,26 +211,14 @@ class AppDatabase extends _$AppDatabase { ); }, from9To10: (m, schema) async { - try { - await m - .dropColumn(schema.preferencesTable, "piped_instance") - .catchError((e) {}); - await m - .dropColumn(schema.preferencesTable, "invidious_instance") - .catchError((e) {}); - await m - .addColumn( - schema.sourceMatchTable, - sourceMatchTable.sourceInfo, - ) - .catchError((e) {}); - await m - .dropColumn(schema.sourceMatchTable, "source_id") - .catchError((e) {}); - } catch (e) { - AppLogger.log.e(e); - return; - } + await m.dropColumn(schema.preferencesTable, "piped_instance"); + await m.dropColumn(schema.preferencesTable, "invidious_instance"); + await m.addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ); + await customStatement("DROP INDEX IF EXISTS uniq_track_match;"); + await m.dropColumn(schema.sourceMatchTable, "source_id"); }, ), ); diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart index 9ef79e9b..66a4959c 100644 --- a/lib/models/database/tables/source_match.dart +++ b/lib/models/database/tables/source_match.dart @@ -1,10 +1,5 @@ part of '../database.dart'; -@TableIndex( - name: "uniq_track_match", - columns: {#trackId, #sourceInfo, #sourceType}, - unique: true, -) class SourceMatchTable extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get trackId => text()(); diff --git a/pubspec.lock b/pubspec.lock index 91f1c2eb..d86cb541 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2002,10 +2002,10 @@ packages: dependency: transitive description: name: random_user_agents - sha256: "19facde509a2482dababb454faf2aceff797a6ae08e80f91268c0c8a7420f03b" + sha256: "95647149687167e82a7b39e1b4616fdebb574981b71b6f0cfca21b69f36293a8" url: "https://pub.dev" source: hosted - version: "1.0.15" + version: "1.0.17" recase: dependency: transitive description: From 7c632c8f06c834d6572f38c43c18b6f7d0e8c601 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 21:56:03 +0600 Subject: [PATCH 26/47] cd: remove unnecessary stuff for android build --- .github/workflows/spotube-release-binary.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 2ddd7a6a..449165e6 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -76,6 +76,14 @@ jobs: cache: true git-source: https://github.com/flutter/flutter.git + - name: free disk space + if: ${{ matrix.platform == 'android' }} + run: | + sudo swapoff -a + sudo rm -f /swapfile + sudo apt clean + docker rmi $(docker image ls -aq) + df -h - name: Setup Java if: ${{matrix.platform == 'android'}} uses: actions/setup-java@v4 From fda2257119a57116315ab2a84b23e81a559b13fb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 7 Nov 2025 22:51:48 +0600 Subject: [PATCH 27/47] feat: add default plugin loading capability --- .../plugin.smplug | Bin 0 -> 91456 bytes .../plugin.smplug | Bin 0 -> 19827 bytes lib/models/database/database.dart | 1 + lib/models/database/tables/preferences.dart | 4 +- .../metadata_plugin_provider.dart | 79 +++++++++++++++++- pubspec.yaml | 2 + 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug create mode 100644 assets/plugins/spotube-plugin-youtube-audio/plugin.smplug diff --git a/assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug b/assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug new file mode 100644 index 0000000000000000000000000000000000000000..41be05a4fd3916e42b4ed6f267be8409ee05707c GIT binary patch literal 91456 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

EPT!^j+33SaT!Cf$%!1)YT^@4|E01+izgxo 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 literal 0 HcmV?d00001 diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 387bcdb7..8df70968 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -22,6 +22,7 @@ import 'package:drift/native.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index cc810ae7..3029e2a8 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -111,7 +111,9 @@ class PreferencesTable extends Table { localLibraryLocation: [], themeMode: ThemeMode.system, audioSourceId: null, - youtubeClientEngine: YoutubeClientEngine.youtubeExplode, + youtubeClientEngine: kIsIOS + ? YoutubeClientEngine.youtubeExplode + : YoutubeClientEngine.newPipe, discordPresence: true, endlessPlayback: true, enableConnect: false, diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index ab3c8547..cdc96c41 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -101,7 +102,11 @@ class MetadataPluginNotifier extends AsyncNotifier { final plugins = await database.pluginsTable.select().get(); - return await toStatePlugins(plugins); + final pluginState = await toStatePlugins(plugins); + + await _loadDefaultPlugins(pluginState); + + return pluginState; } Future toStatePlugins( @@ -171,6 +176,45 @@ class MetadataPluginNotifier extends AsyncNotifier { ); } + Future _loadDefaultPlugins(MetadataPluginState pluginState) async { + const plugins = [ + "spotube-plugin-musicbrainz-listenbrainz", + "spotube-plugin-youtube-audio", + ]; + + for (final plugin in plugins) { + final byteData = await rootBundle.load( + "assets/plugins/$plugin/plugin.smplug", + ); + final pluginConfig = + await extractPluginArchive(byteData.buffer.asUint8List()); + try { + await addPlugin(pluginConfig); + } on MetadataPluginException catch (e) { + if (e.errorCode == MetadataPluginErrorCode.duplicatePlugin && + await isPluginUpdate(pluginConfig)) { + final oldConfig = pluginState.plugins + .firstWhereOrNull((p) => p.slug == pluginConfig.slug); + if (oldConfig == null) continue; + final isDefaultMetadata = + oldConfig == pluginState.defaultMetadataPluginConfig; + final isDefaultAudioSource = + oldConfig == pluginState.defaultAudioSourcePluginConfig; + + await removePlugin(pluginConfig); + await addPlugin(pluginConfig); + + if (isDefaultMetadata) { + await setDefaultMetadataPlugin(pluginConfig); + } + if (isDefaultAudioSource) { + await setDefaultAudioSourcePlugin(pluginConfig); + } + } + } + } + } + Uri _getGithubReleasesUrl(String repoUrl) { final parsedUri = Uri.parse(repoUrl); final uri = parsedUri.replace( @@ -373,11 +417,19 @@ class MetadataPluginNotifier extends AsyncNotifier { repository: Value(plugin.repository), // Setting the very first plugin as the default plugin selectedForMetadata: Value( - (state.valueOrNull?.plugins.isEmpty ?? true) && + (state.valueOrNull?.plugins + .where( + (d) => d.abilities.contains(PluginAbilities.metadata)) + .isEmpty ?? + true) && plugin.abilities.contains(PluginAbilities.metadata), ), selectedForAudioSource: Value( - (state.valueOrNull?.plugins.isEmpty ?? true) && + (state.valueOrNull?.plugins + .where((d) => + d.abilities.contains(PluginAbilities.audioSource)) + .isEmpty ?? + true) && plugin.abilities.contains(PluginAbilities.audioSource), ), ), @@ -420,6 +472,27 @@ class MetadataPluginNotifier extends AsyncNotifier { } } + Future isPluginUpdate(PluginConfiguration newPlugin) async { + final pluginRes = await (database.pluginsTable.select() + ..where( + (tbl) => + tbl.name.equals(newPlugin.name) & + tbl.author.equals(newPlugin.author), + ) + ..limit(1)) + .get(); + + if (pluginRes.isEmpty) { + return false; + } + + final oldPlugin = pluginRes.first; + final oldPluginApiVersion = Version.parse(oldPlugin.pluginApiVersion); + final newPluginApiVersion = Version.parse(newPlugin.pluginApiVersion); + + return newPluginApiVersion > oldPluginApiVersion; + } + Future updatePlugin( PluginConfiguration plugin, PluginUpdateAvailable update, diff --git a/pubspec.yaml b/pubspec.yaml index 0c31a0cb..fd3d78c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -227,6 +227,8 @@ flutter: - assets/branding/spotube-logo.png - assets/branding/spotube-logo-light.png - assets/branding/spotube-logo.ico + - assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug + - assets/plugins/spotube-plugin-youtube-audio/plugin.smplug - LICENSE - packages/flutter_undraw/assets/undraw/access_denied.svg - packages/flutter_undraw/assets/undraw/fixing_bugs.svg From d2dd60aa5c6391f70c369887de90254cd1ed0b6a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 8 Nov 2025 13:48:50 +0600 Subject: [PATCH 28/47] chore: update YoutubeExplode to v3 --- .../youtube_engine/quickjs_solver.dart | 167 ++++++++++++++++++ .../youtube_explode_engine.dart | 5 +- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 16 +- pubspec.yaml | 3 +- windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 lib/services/youtube_engine/quickjs_solver.dart diff --git a/lib/services/youtube_engine/quickjs_solver.dart b/lib/services/youtube_engine/quickjs_solver.dart new file mode 100644 index 00000000..4e8bfafb --- /dev/null +++ b/lib/services/youtube_engine/quickjs_solver.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:youtube_explode_dart/js_challenge.dart'; +// ignore: implementation_imports +import 'package:youtube_explode_dart/src/reverse_engineering/challenges/ejs/ejs.dart'; +import 'package:jsf/jsf.dart'; + +/// [WIP] +class QuickJSEJSSolver extends BaseJSChallengeSolver { + final _playerCache = {}; + final _sigCache = <(String, String, JSChallengeType), String>{}; + final QuickJSRuntime qjs; + QuickJSEJSSolver._(this.qjs); + + static Future init() async { + final modules = await EJSBuilder.getJSModules(); + final deno = await QuickJSRuntime.init(modules); + return QuickJSEJSSolver._(deno); + } + + @override + Future solve( + String playerUrl, JSChallengeType type, String challenge) async { + final key = (playerUrl, challenge, type); + if (_sigCache.containsKey(key)) { + return _sigCache[key]!; + } + + var playerScript = _playerCache[playerUrl]; + if (playerScript == null) { + final resp = await http.get(Uri.parse(playerUrl)); + playerScript = _playerCache[playerUrl] = resp.body; + } + final jsCall = EJSBuilder.buildJSCall(playerScript, { + type: [challenge], + }); + + final result = await qjs.eval(jsCall); + // Trim the first and last characters (' delimiters of the JS string) + final data = json.decode(result.substring(1, result.length - 1)) + as Map; + + if (data['type'] != 'result') { + throw Exception('Unexpected response type: ${data['type']}'); + } + final response = data['responses'][0]; + if (response['type'] != 'result') { + throw Exception('Unexpected item response type: ${response['type']}'); + } + final decoded = response['data'][challenge]; + if (decoded == null) { + throw Exception('No data for challenge: $challenge'); + } + + _sigCache[key] = decoded; + + return decoded; + } + + @override + void dispose() { + qjs.dispose(); + } +} + +class _EvalRequest { + final String code; + final Completer completer; + + _EvalRequest(this.code, this.completer); +} + +class QuickJSRuntime { + final JsRuntime _runtime; + final StreamController _stdoutController = + StreamController.broadcast(); + + // Queue for incoming eval requests + final Queue<_EvalRequest> _evalQueue = Queue<_EvalRequest>(); + bool _isProcessing = false; // Flag to indicate if an eval is currently active + + QuickJSRuntime(this._runtime); + + /// Disposes the Deno process. + void dispose() { + _stdoutController.close(); + _runtime.dispose(); + } + + /// Sends JavaScript code to Deno for evaluation. + /// Assumes single-line input produces single-line output. + Future eval(String code) { + final completer = Completer(); + final request = _EvalRequest(code, completer); + _evalQueue.addLast(request); // Add request to the end of the queue + _processQueue(); // Attempt to process the queue + + return completer.future; + } + + // Processes the eval queue. + void _processQueue() { + if (_isProcessing || _evalQueue.isEmpty) { + return; // Already processing or nothing in queue + } + + _isProcessing = true; + final request = + _evalQueue.first; // Get the next request without removing it yet + + StreamSubscription? currentOutputSubscription; + Completer lineReceived = Completer(); + + currentOutputSubscription = _stdoutController.stream.listen((data) { + if (!lineReceived.isCompleted) { + // Assuming single line output per eval. + // This will capture the first full line or chunk received after sending the code. + request.completer.complete(data.trim()); + lineReceived.complete(); + currentOutputSubscription + ?.cancel(); // Cancel subscription for this request + _evalQueue.removeFirst(); // Remove the processed request + _isProcessing = false; // Mark as no longer processing + _processQueue(); // Attempt to process next item in queue + } + }, onError: (e) { + if (!request.completer.isCompleted) { + request.completer.completeError(e); + lineReceived.completeError(e); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }, onDone: () { + if (!request.completer.isCompleted) { + request.completer.completeError( + StateError('Deno process closed while awaiting eval result.')); + lineReceived.completeError( + StateError('Deno process closed while awaiting eval result.')); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }); + + debugPrint("[QuickJS Solver] Evaluate ${request.code}"); + final result = _runtime.eval(request.code); + debugPrint("[QuickJS Solver] Evaluation Result $result"); + _stdoutController.add(result); + } + + static Future init(String initCode) async { + debugPrint("[QuickJS Solver] Initializing"); + debugPrint("[QuickJS Solver] script $initCode"); + + final runtime = JsRuntime(); + + runtime.execInitScript(initCode); + + return QuickJSRuntime(runtime); + } +} diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index c552f883..f8587ca6 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -2,6 +2,7 @@ import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:spotube/services/youtube_engine/youtube_engine.dart'; +// import 'package:youtube_explode_dart/solvers.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'dart:async'; @@ -55,8 +56,9 @@ class IsolatedYoutubeExplode { } } - static void _isolateEntry(SendPort mainSendPort) { + static Future _isolateEntry(SendPort mainSendPort) async { final receivePort = ReceivePort(); + // final solver = await DenoEJSSolver.init(); final youtubeExplode = YoutubeExplode(); final stopWatch = kDebugMode ? Stopwatch() : null; @@ -163,6 +165,7 @@ class YouTubeExplodeEngine implements YouTubeEngine { ytClients: [ YoutubeApiClient.ios, YoutubeApiClient.androidVr, + YoutubeApiClient.android, ], ); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e5c8a845..541826e6 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -24,6 +24,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god ) diff --git a/pubspec.lock b/pubspec.lock index d86cb541..7e53e91c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -446,10 +446,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -1439,6 +1439,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + jsf: + dependency: "direct main" + description: + name: jsf + sha256: "189ba3b9216702f9b6f2d8ea90fa5acaca13bbe5dd2f72fb38618005b41a737f" + url: "https://pub.dev" + source: hosted + version: "0.6.1" json_annotation: dependency: "direct main" description: @@ -2840,10 +2848,10 @@ packages: dependency: "direct main" description: name: youtube_explode_dart - sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0" + sha256: add33de45d80c7f71a5e3dd464dd82fafd7fb5ab875fd303c023f30f76618325 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "3.0.0" yt_dlp_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fd3d78c5..9b84196b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -131,7 +131,7 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: ^2.5.3 + youtube_explode_dart: ^3.0.0 yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git @@ -161,6 +161,7 @@ dependencies: flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 change_case: ^1.1.0 + jsf: ^0.6.1 dev_dependencies: build_runner: ^2.4.13 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6e831cf5..53cd3667 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -27,6 +27,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god smtc_windows ) From 3209c75144dbb7b6d3fbc2a3c1aff63feb3494c9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 8 Nov 2025 15:49:37 +0600 Subject: [PATCH 29/47] fix: downloaded tracks are not tagged with metadata --- README.md | 1 - assets/images/logos/songlink-transparent.png | Bin 8758 -> 0 bytes lib/collections/assets.gen.dart | 41 ++- lib/components/track_tile/track_options.dart | 20 -- lib/models/database/database.g.dart | 5 +- lib/models/metadata/audio_source.dart | 10 + lib/models/metadata/metadata.freezed.dart | 16 +- lib/pages/library/user_downloads.dart | 4 +- .../user_local_tracks/local_folder.dart | 57 +-- lib/provider/download_manager_provider.dart | 57 +-- lib/provider/server/routes/playback.dart | 5 +- .../track_options/track_options_provider.dart | 10 +- lib/services/audio_player/audio_player.dart | 2 +- lib/services/song_link/model.dart | 19 - lib/services/song_link/song_link.dart | 54 --- lib/services/song_link/song_link.freezed.dart | 333 ------------------ lib/services/song_link/song_link.g.dart | 32 -- 17 files changed, 112 insertions(+), 554 deletions(-) delete mode 100644 assets/images/logos/songlink-transparent.png delete mode 100644 lib/services/song_link/model.dart delete mode 100644 lib/services/song_link/song_link.dart delete mode 100644 lib/services/song_link/song_link.freezed.dart delete mode 100644 lib/services/song_link/song_link.g.dart diff --git a/README.md b/README.md index 4c5c1f1c..153832ea 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,6 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube. 1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader. 1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites. -1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content 1. [LRCLib](https://lrclib.net/) - A public synced lyric API. 1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution 1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users diff --git a/assets/images/logos/songlink-transparent.png b/assets/images/logos/songlink-transparent.png deleted file mode 100644 index fc4ae54146e35ee36d490dba7541dda3b51f6c49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8758 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 diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index 7fa75e1d..7ab0ad03 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -66,6 +66,19 @@ class $AssetsImagesGen { ]; } +class $AssetsPluginsGen { + const $AssetsPluginsGen(); + + /// Directory path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz + $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen + get spotubePluginMusicbrainzListenbrainz => + const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen(); + + /// Directory path: assets/plugins/spotube-plugin-youtube-audio + $AssetsPluginsSpotubePluginYoutubeAudioGen get spotubePluginYoutubeAudio => + const $AssetsPluginsSpotubePluginYoutubeAudioGen(); +} + class $AssetsImagesLogosGen { const $AssetsImagesLogosGen(); @@ -81,13 +94,30 @@ class $AssetsImagesLogosGen { AssetGenImage get jiosaavn => const AssetGenImage('assets/images/logos/jiosaavn.png'); - /// File path: assets/images/logos/songlink-transparent.png - AssetGenImage get songlinkTransparent => - const AssetGenImage('assets/images/logos/songlink-transparent.png'); + /// List of all assets + List get values => [dabMusic, invidious, jiosaavn]; +} + +class $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen { + const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen(); + + /// File path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug + String get plugin => + 'assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug'; /// List of all assets - List get values => - [dabMusic, invidious, jiosaavn, songlinkTransparent]; + List get values => [plugin]; +} + +class $AssetsPluginsSpotubePluginYoutubeAudioGen { + const $AssetsPluginsSpotubePluginYoutubeAudioGen(); + + /// File path: assets/plugins/spotube-plugin-youtube-audio/plugin.smplug + String get plugin => + 'assets/plugins/spotube-plugin-youtube-audio/plugin.smplug'; + + /// List of all assets + List get values => [plugin]; } class Assets { @@ -96,6 +126,7 @@ class Assets { static const String license = 'LICENSE'; static const $AssetsBrandingGen branding = $AssetsBrandingGen(); static const $AssetsImagesGen images = $AssetsImagesGen(); + static const $AssetsPluginsGen plugins = $AssetsPluginsGen(); /// List of all assets static List get values => [license]; diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 6124abf0..61b47968 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; -import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/ui/button_tile.dart'; @@ -36,7 +35,6 @@ class TrackOptions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final mediaQuery = MediaQuery.of(context); - final ThemeData(:colorScheme) = Theme.of(context); final trackOptionActions = ref.watch(trackOptionActionsProvider(track)); final ( @@ -260,24 +258,6 @@ class TrackOptions extends HookConsumerWidget { leading: const Icon(SpotubeIcons.share), title: Text(context.l10n.share), ), - if (!isLocalTrack) - ButtonTile( - style: ButtonVariance.menu, - onPressed: () async { - await trackOptionActions.action( - rootNavigatorKey.currentContext!, - TrackOptionValue.songlink, - playlistId, - ); - onTapItem?.call(); - }, - leading: Assets.images.logos.songlinkTransparent.image( - width: 22, - height: 22, - color: colorScheme.foreground.withValues(alpha: 0.5), - ), - title: Text(context.l10n.song_link), - ), if (!isLocalTrack) ButtonTile( style: ButtonVariance.menu, diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 4a9a7eba..8aa14899 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -4143,8 +4143,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $PluginsTableTable pluginsTable = $PluginsTableTable(this); late final Index uniqueBlacklist = Index('unique_blacklist', 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); - late final Index uniqTrackMatch = Index('uniq_track_match', - 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4160,8 +4158,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { historyTable, lyricsTable, pluginsTable, - uniqueBlacklist, - uniqTrackMatch + uniqueBlacklist ]; } diff --git a/lib/models/metadata/audio_source.dart b/lib/models/metadata/audio_source.dart index 898300e9..4fb790ea 100644 --- a/lib/models/metadata/audio_source.dart +++ b/lib/models/metadata/audio_source.dart @@ -10,6 +10,8 @@ enum SpotubeMediaCompressionType { @Freezed(unionKey: 'type') class SpotubeAudioSourceContainerPreset with _$SpotubeAudioSourceContainerPreset { + const SpotubeAudioSourceContainerPreset._(); + @FreezedUnionValue("lossy") factory SpotubeAudioSourceContainerPreset.lossy({ required SpotubeMediaCompressionType type, @@ -27,6 +29,14 @@ class SpotubeAudioSourceContainerPreset factory SpotubeAudioSourceContainerPreset.fromJson( Map json) => _$SpotubeAudioSourceContainerPresetFromJson(json); + + String getFileExtension() { + return switch (name) { + "mp4" => "m4a", + "webm" => "weba", + _ => name, + }; + } } @freezed diff --git a/lib/models/metadata/metadata.freezed.dart b/lib/models/metadata/metadata.freezed.dart index 301929a5..fee1cbc2 100644 --- a/lib/models/metadata/metadata.freezed.dart +++ b/lib/models/metadata/metadata.freezed.dart @@ -197,12 +197,13 @@ class __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioSourceContainerPresetLossyImpl - implements SpotubeAudioSourceContainerPresetLossy { + extends SpotubeAudioSourceContainerPresetLossy { _$SpotubeAudioSourceContainerPresetLossyImpl( {required this.type, required this.name, required final List qualities}) - : _qualities = qualities; + : _qualities = qualities, + super._(); factory _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson( Map json) => @@ -338,12 +339,13 @@ class _$SpotubeAudioSourceContainerPresetLossyImpl } abstract class SpotubeAudioSourceContainerPresetLossy - implements SpotubeAudioSourceContainerPreset { + extends SpotubeAudioSourceContainerPreset { factory SpotubeAudioSourceContainerPresetLossy( {required final SpotubeMediaCompressionType type, required final String name, required final List qualities}) = _$SpotubeAudioSourceContainerPresetLossyImpl; + SpotubeAudioSourceContainerPresetLossy._() : super._(); factory SpotubeAudioSourceContainerPresetLossy.fromJson( Map json) = @@ -419,12 +421,13 @@ class __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SpotubeAudioSourceContainerPresetLosslessImpl - implements SpotubeAudioSourceContainerPresetLossless { + extends SpotubeAudioSourceContainerPresetLossless { _$SpotubeAudioSourceContainerPresetLosslessImpl( {required this.type, required this.name, required final List qualities}) - : _qualities = qualities; + : _qualities = qualities, + super._(); factory _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson( Map json) => @@ -561,12 +564,13 @@ class _$SpotubeAudioSourceContainerPresetLosslessImpl } abstract class SpotubeAudioSourceContainerPresetLossless - implements SpotubeAudioSourceContainerPreset { + extends SpotubeAudioSourceContainerPreset { factory SpotubeAudioSourceContainerPresetLossless( {required final SpotubeMediaCompressionType type, required final String name, required final List qualities}) = _$SpotubeAudioSourceContainerPresetLosslessImpl; + SpotubeAudioSourceContainerPresetLossless._() : super._(); factory SpotubeAudioSourceContainerPresetLossless.fromJson( Map json) = diff --git a/lib/pages/library/user_downloads.dart b/lib/pages/library/user_downloads.dart index 6566bed6..73dc692f 100644 --- a/lib/pages/library/user_downloads.dart +++ b/lib/pages/library/user_downloads.dart @@ -16,7 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final downloadManager = ref.watch(downloadManagerProvider); - final history = downloadManager.$backHistory; + final history = downloadManager.$history; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -48,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget { child: ListView.builder( itemCount: history.length, itemBuilder: (context, index) { - return DownloadItem(track: history.elementAt(index)); + return DownloadItem(track: history.elementAt(index).query); }, ), ), diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index 27af0f57..58a7a023 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -346,36 +346,41 @@ class LocalLibraryPage extends HookConsumerWidget { controller: controller, child: Skeletonizer( enabled: trackSnapshot.isLoading, - child: ListView.builder( + child: CustomScrollView( controller: controller, physics: const AlwaysScrollableScrollPhysics(), - itemCount: trackSnapshot.isLoading - ? 5 - : filteredTracks.length, - itemBuilder: (context, index) { - if (trackSnapshot.isLoading) { - return TrackTile( - playlist: playlist, - track: FakeData.track, - index: index, - ); - } + slivers: [ + SliverList.builder( + itemCount: trackSnapshot.isLoading + ? 5 + : filteredTracks.length, + itemBuilder: (context, index) { + if (trackSnapshot.isLoading) { + return TrackTile( + playlist: playlist, + track: FakeData.track, + index: index, + ); + } - final track = filteredTracks[index]; - return TrackTile( - index: index, - playlist: playlist, - track: track, - userPlaylist: false, - onTap: () async { - await playLocalTracks( - ref, - sortedTracks, - currentTrack: track, + final track = filteredTracks[index]; + return TrackTile( + index: index, + playlist: playlist, + track: track, + userPlaylist: false, + onTap: () async { + await playLocalTracks( + ref, + sortedTracks, + currentTrack: track, + ); + }, ); }, - ); - }, + ), + const SliverGap(200), + ], ), ), ), @@ -398,7 +403,7 @@ class LocalLibraryPage extends HookConsumerWidget { error: (error, stackTrace) => Text(error.toString() + stackTrace.toString()), ); - }) + }), ], ), ), diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index bc1de813..d64da32e 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -19,7 +19,6 @@ import 'package:spotube/utils/service_utils.dart'; class DownloadManagerProvider extends ChangeNotifier { DownloadManagerProvider({required this.ref}) : $history = {}, - $backHistory = {}, dl = DownloadManager() { dl.statusStream.listen((event) async { try { @@ -28,14 +27,13 @@ class DownloadManagerProvider extends ChangeNotifier { final sourcedTrack = $history.firstWhereOrNull( (element) => element.getUrlOfQuality( - downloadContainer, downloadQualityIndex) == + downloadContainer, + downloadQualityIndex, + ) == request.url, ); + if (sourcedTrack == null) return; - final track = $backHistory.firstWhereOrNull( - (element) => element.id == sourcedTrack.query.id, - ); - if (track == null) return; final savePath = getTrackFileUrl(sourcedTrack); // related to onFileExists @@ -47,12 +45,12 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.exists()) { await oldFile.rename(savePath); } + if (status != DownloadStatus.completed || //? WebA audiotagging is not supported yet //? Although in future by converting weba to opus & then tagging it //? is possible using vorbis comments - downloadContainer.name == "weba" || - downloadContainer.name == "webm") { + downloadContainer.getFileExtension() == "weba") { return; } @@ -63,13 +61,13 @@ class DownloadManagerProvider extends ChangeNotifier { } final imageBytes = await ServiceUtils.downloadImage( - (track.album.images).asUrlString( + (sourcedTrack.query.album.images).asUrlString( placeholder: ImagePlaceholder.albumArt, index: 1, ), ); - final metadata = track.toMetadata( + final metadata = sourcedTrack.query.toMetadata( fileLength: await file.length(), imageBytes: imageBytes, ); @@ -111,17 +109,16 @@ class DownloadManagerProvider extends ChangeNotifier { final Set $history; // these are the tracks which metadata hasn't been fetched yet - final Set $backHistory; final DownloadManager dl; String getTrackFileUrl(SourcedTrack track) { final name = - "${track.query.name} - ${track.query.artists.join(", ")}.${downloadContainer.name}"; + "${track.query.name} - ${track.query.artists.map((e) => e.name).join(", ")}.${downloadContainer.getFileExtension()}"; return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } bool isActive(SpotubeFullTrackObject track) { - if ($backHistory.contains(track)) return true; + if ($history.any((e) => e.query.id == track.id)) return true; final sourcedTrack = $history.firstWhereOrNull( (element) => element.query.id == track.id, @@ -146,9 +143,7 @@ class DownloadManagerProvider extends ChangeNotifier { /// For singular downloads Future addToQueue(SpotubeFullTrackObject track) async { - final sourcedTrack = await ref.read( - sourcedTrackProvider(track).future, - ); + final sourcedTrack = await ref.read(sourcedTrackProvider(track).future); final savePath = getTrackFileUrl(sourcedTrack); @@ -161,35 +156,17 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.rename("$savePath.old"); } - if (sourcedTrack.qualityPreset == downloadContainer) { - final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, - savePath, - ); - if (downloadTask != null) { - $history.add(sourcedTrack); - } - } else { - $backHistory.add(track); - final sourcedTrack = - await ref.read(sourcedTrackProvider(track).future).then((d) { - $backHistory.remove(track); - return d; - }); - final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, - savePath, - ); - if (downloadTask != null) { - $history.add(sourcedTrack); - } + final downloadTask = await dl.addDownload( + sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, + savePath, + ); + if (downloadTask != null) { + $history.add(sourcedTrack); } - notifyListeners(); } Future batchAddToQueue(List tracks) async { - $backHistory.addAll(tracks); notifyListeners(); for (final track in tracks) { try { diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 792d7797..f7085505 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -48,7 +48,7 @@ class ServerPlaybackRoutes { return join( await UserPreferencesNotifier.getMusicCacheDir(), ServiceUtils.sanitizeFilename( - '${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.name}', + '${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.getFileExtension()}', ), ); } @@ -263,8 +263,7 @@ class ServerPlaybackRoutes { } if (contentRange.total == fileLength && - track.qualityPreset!.name != "webm" || - track.qualityPreset!.name != "weba") { + track.qualityPreset!.getFileExtension() != "weba") { final playlistTrack = playlist.tracks.firstWhereOrNull( (element) => element.id == track.query.id, ); diff --git a/lib/provider/track_options/track_options_provider.dart b/lib/provider/track_options/track_options_provider.dart index 42f363d9..e6b05201 100644 --- a/lib/provider/track_options/track_options_provider.dart +++ b/lib/provider/track_options/track_options_provider.dart @@ -21,12 +21,10 @@ import 'package:spotube/provider/metadata_plugin/library/playlists.dart'; import 'package:spotube/provider/metadata_plugin/library/tracks.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/services/metadata/errors/exceptions.dart'; -import 'package:url_launcher/url_launcher_string.dart'; enum TrackOptionValue { album, share, - songlink, addToPlaylist, addToQueue, removeFromPlaylist, @@ -237,10 +235,6 @@ class TrackOptionsActions { case TrackOptionValue.share: actionShare(context); break; - case TrackOptionValue.songlink: - final url = "https://song.link/s/${track.id}"; - await launchUrlString(url); - break; case TrackOptionValue.details: if (track is! SpotubeFullTrackObject) break; showDialog( @@ -252,8 +246,8 @@ class TrackOptionsActions { ); break; case TrackOptionValue.download: - if (track is! SpotubeFullTrackObject) break; - await downloadManager.addToQueue(track as SpotubeFullTrackObject); + if (track is SpotubeLocalTrackObject) break; + downloadManager.addToQueue(track as SpotubeFullTrackObject); break; case TrackOptionValue.startRadio: actionStartRadio(context); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index a30fafba..9ae4e973 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -37,7 +37,7 @@ class SpotubeMedia extends mk.Media { factory SpotubeMedia.media(Media media) { assert(media.extras != null, "[Media] must have extra metadata set"); - return SpotubeMedia(SpotubeFullTrackObject.fromJson(media.extras!)); + return SpotubeMedia(SpotubeTrackObject.fromJson(media.extras!)); } } diff --git a/lib/services/song_link/model.dart b/lib/services/song_link/model.dart deleted file mode 100644 index ae9d3833..00000000 --- a/lib/services/song_link/model.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of './song_link.dart'; - -@freezed -class SongLink with _$SongLink { - const factory SongLink({ - required String displayName, - required String linkId, - required String platform, - required bool show, - required String? uniqueId, - required String? country, - required String? url, - required String? nativeAppUriMobile, - required String? nativeAppUriDesktop, - }) = _SongLink; - - factory SongLink.fromJson(Map json) => - _$SongLinkFromJson(json); -} diff --git a/lib/services/song_link/song_link.dart b/lib/services/song_link/song_link.dart deleted file mode 100644 index e3cffa52..00000000 --- a/lib/services/song_link/song_link.dart +++ /dev/null @@ -1,54 +0,0 @@ -library song_link; - -import 'dart:convert'; - -import 'package:spotube/services/logger/logger.dart'; -import 'package:dio/dio.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:html/parser.dart'; - -part 'model.dart'; - -part 'song_link.freezed.dart'; -part 'song_link.g.dart'; - -abstract class SongLinkService { - static final dio = Dio(); - static Future> links(String spotifyId) async { - try { - final res = await dio.get( - "https://song.link/s/$spotifyId", - options: Options( - headers: { - "Accept": - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" - }, - responseType: ResponseType.plain, - ), - ); - - final document = parse(res.data); - - final script = document.getElementById("__NEXT_DATA__")?.text; - - if (script == null) { - return []; - } - - final pageProps = jsonDecode(script) as Map; - final songLinks = pageProps["props"]?["pageProps"]?["pageData"] - ?["sections"] - ?.firstWhere( - (section) => section?["sectionId"] == "section|auto|links|listen", - )?["links"] as List?; - - return songLinks?.map((link) => SongLink.fromJson(link)).toList() ?? - []; - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - return []; - } - } -} diff --git a/lib/services/song_link/song_link.freezed.dart b/lib/services/song_link/song_link.freezed.dart deleted file mode 100644 index c704cde3..00000000 --- a/lib/services/song_link/song_link.freezed.dart +++ /dev/null @@ -1,333 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'song_link.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -SongLink _$SongLinkFromJson(Map json) { - return _SongLink.fromJson(json); -} - -/// @nodoc -mixin _$SongLink { - String get displayName => throw _privateConstructorUsedError; - String get linkId => throw _privateConstructorUsedError; - String get platform => throw _privateConstructorUsedError; - bool get show => throw _privateConstructorUsedError; - String? get uniqueId => throw _privateConstructorUsedError; - String? get country => throw _privateConstructorUsedError; - String? get url => throw _privateConstructorUsedError; - String? get nativeAppUriMobile => throw _privateConstructorUsedError; - String? get nativeAppUriDesktop => throw _privateConstructorUsedError; - - /// Serializes this SongLink to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SongLink - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SongLinkCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SongLinkCopyWith<$Res> { - factory $SongLinkCopyWith(SongLink value, $Res Function(SongLink) then) = - _$SongLinkCopyWithImpl<$Res, SongLink>; - @useResult - $Res call( - {String displayName, - String linkId, - String platform, - bool show, - String? uniqueId, - String? country, - String? url, - String? nativeAppUriMobile, - String? nativeAppUriDesktop}); -} - -/// @nodoc -class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink> - implements $SongLinkCopyWith<$Res> { - _$SongLinkCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SongLink - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? displayName = null, - Object? linkId = null, - Object? platform = null, - Object? show = null, - Object? uniqueId = freezed, - Object? country = freezed, - Object? url = freezed, - Object? nativeAppUriMobile = freezed, - Object? nativeAppUriDesktop = freezed, - }) { - return _then(_value.copyWith( - displayName: null == displayName - ? _value.displayName - : displayName // ignore: cast_nullable_to_non_nullable - as String, - linkId: null == linkId - ? _value.linkId - : linkId // ignore: cast_nullable_to_non_nullable - as String, - platform: null == platform - ? _value.platform - : platform // ignore: cast_nullable_to_non_nullable - as String, - show: null == show - ? _value.show - : show // ignore: cast_nullable_to_non_nullable - as bool, - uniqueId: freezed == uniqueId - ? _value.uniqueId - : uniqueId // ignore: cast_nullable_to_non_nullable - as String?, - country: freezed == country - ? _value.country - : country // ignore: cast_nullable_to_non_nullable - as String?, - url: freezed == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String?, - nativeAppUriMobile: freezed == nativeAppUriMobile - ? _value.nativeAppUriMobile - : nativeAppUriMobile // ignore: cast_nullable_to_non_nullable - as String?, - nativeAppUriDesktop: freezed == nativeAppUriDesktop - ? _value.nativeAppUriDesktop - : nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SongLinkImplCopyWith<$Res> - implements $SongLinkCopyWith<$Res> { - factory _$$SongLinkImplCopyWith( - _$SongLinkImpl value, $Res Function(_$SongLinkImpl) then) = - __$$SongLinkImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String displayName, - String linkId, - String platform, - bool show, - String? uniqueId, - String? country, - String? url, - String? nativeAppUriMobile, - String? nativeAppUriDesktop}); -} - -/// @nodoc -class __$$SongLinkImplCopyWithImpl<$Res> - extends _$SongLinkCopyWithImpl<$Res, _$SongLinkImpl> - implements _$$SongLinkImplCopyWith<$Res> { - __$$SongLinkImplCopyWithImpl( - _$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then) - : super(_value, _then); - - /// Create a copy of SongLink - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? displayName = null, - Object? linkId = null, - Object? platform = null, - Object? show = null, - Object? uniqueId = freezed, - Object? country = freezed, - Object? url = freezed, - Object? nativeAppUriMobile = freezed, - Object? nativeAppUriDesktop = freezed, - }) { - return _then(_$SongLinkImpl( - displayName: null == displayName - ? _value.displayName - : displayName // ignore: cast_nullable_to_non_nullable - as String, - linkId: null == linkId - ? _value.linkId - : linkId // ignore: cast_nullable_to_non_nullable - as String, - platform: null == platform - ? _value.platform - : platform // ignore: cast_nullable_to_non_nullable - as String, - show: null == show - ? _value.show - : show // ignore: cast_nullable_to_non_nullable - as bool, - uniqueId: freezed == uniqueId - ? _value.uniqueId - : uniqueId // ignore: cast_nullable_to_non_nullable - as String?, - country: freezed == country - ? _value.country - : country // ignore: cast_nullable_to_non_nullable - as String?, - url: freezed == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String?, - nativeAppUriMobile: freezed == nativeAppUriMobile - ? _value.nativeAppUriMobile - : nativeAppUriMobile // ignore: cast_nullable_to_non_nullable - as String?, - nativeAppUriDesktop: freezed == nativeAppUriDesktop - ? _value.nativeAppUriDesktop - : nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable - as String?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$SongLinkImpl implements _SongLink { - const _$SongLinkImpl( - {required this.displayName, - required this.linkId, - required this.platform, - required this.show, - required this.uniqueId, - required this.country, - required this.url, - required this.nativeAppUriMobile, - required this.nativeAppUriDesktop}); - - factory _$SongLinkImpl.fromJson(Map json) => - _$$SongLinkImplFromJson(json); - - @override - final String displayName; - @override - final String linkId; - @override - final String platform; - @override - final bool show; - @override - final String? uniqueId; - @override - final String? country; - @override - final String? url; - @override - final String? nativeAppUriMobile; - @override - final String? nativeAppUriDesktop; - - @override - String toString() { - return 'SongLink(displayName: $displayName, linkId: $linkId, platform: $platform, show: $show, uniqueId: $uniqueId, country: $country, url: $url, nativeAppUriMobile: $nativeAppUriMobile, nativeAppUriDesktop: $nativeAppUriDesktop)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SongLinkImpl && - (identical(other.displayName, displayName) || - other.displayName == displayName) && - (identical(other.linkId, linkId) || other.linkId == linkId) && - (identical(other.platform, platform) || - other.platform == platform) && - (identical(other.show, show) || other.show == show) && - (identical(other.uniqueId, uniqueId) || - other.uniqueId == uniqueId) && - (identical(other.country, country) || other.country == country) && - (identical(other.url, url) || other.url == url) && - (identical(other.nativeAppUriMobile, nativeAppUriMobile) || - other.nativeAppUriMobile == nativeAppUriMobile) && - (identical(other.nativeAppUriDesktop, nativeAppUriDesktop) || - other.nativeAppUriDesktop == nativeAppUriDesktop)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, displayName, linkId, platform, - show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop); - - /// Create a copy of SongLink - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => - __$$SongLinkImplCopyWithImpl<_$SongLinkImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SongLinkImplToJson( - this, - ); - } -} - -abstract class _SongLink implements SongLink { - const factory _SongLink( - {required final String displayName, - required final String linkId, - required final String platform, - required final bool show, - required final String? uniqueId, - required final String? country, - required final String? url, - required final String? nativeAppUriMobile, - required final String? nativeAppUriDesktop}) = _$SongLinkImpl; - - factory _SongLink.fromJson(Map json) = - _$SongLinkImpl.fromJson; - - @override - String get displayName; - @override - String get linkId; - @override - String get platform; - @override - bool get show; - @override - String? get uniqueId; - @override - String? get country; - @override - String? get url; - @override - String? get nativeAppUriMobile; - @override - String? get nativeAppUriDesktop; - - /// Create a copy of SongLink - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/services/song_link/song_link.g.dart b/lib/services/song_link/song_link.g.dart deleted file mode 100644 index 7658a74c..00000000 --- a/lib/services/song_link/song_link.g.dart +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'song_link.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$SongLinkImpl _$$SongLinkImplFromJson(Map json) => _$SongLinkImpl( - displayName: json['displayName'] as String, - linkId: json['linkId'] as String, - platform: json['platform'] as String, - show: json['show'] as bool, - uniqueId: json['uniqueId'] as String?, - country: json['country'] as String?, - url: json['url'] as String?, - nativeAppUriMobile: json['nativeAppUriMobile'] as String?, - nativeAppUriDesktop: json['nativeAppUriDesktop'] as String?, - ); - -Map _$$SongLinkImplToJson(_$SongLinkImpl instance) => - { - 'displayName': instance.displayName, - 'linkId': instance.linkId, - 'platform': instance.platform, - 'show': instance.show, - 'uniqueId': instance.uniqueId, - 'country': instance.country, - 'url': instance.url, - 'nativeAppUriMobile': instance.nativeAppUriMobile, - 'nativeAppUriDesktop': instance.nativeAppUriDesktop, - }; From f10a3d4976c96f815f7aca79206a57c8bd06e4e8 Mon Sep 17 00:00:00 2001 From: Rahul Sahani <110347707+Rahul-Sahani04@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:06:10 +0530 Subject: [PATCH 30/47] feat(queue): add multi-select and bulk actions to queue (#2839) * feat(queue): add multi-select and bulk actions to queue - Add selection mode to PlayerQueue with long-press to select - Disable inner navigation (title/artist) when selecting via TrackTile - Show checkboxes only in selection mode - Add selection AppBar behavior and bottom-sheet menu with: Select all, Add to playlist, Remove selected, Cancel - Reuse existing PlaylistAddTrackDialog for bulk add - Hide drag handle while in selection mode Closes: # (implement multi-select queue feature) * chore: update .gitignore to include .vscode and modify signing configurations back to default in build.gradle * chore: add VS Code configuration files * chore: update dependencies in pubspec.lock * chore: update pubspec.lock to reflect dependency changes and version updates * chore: fix replace material widgets with shadcn widgets --------- Co-authored-by: Kingkor Roy Tirtho --- .gitignore | 1 + lib/components/track_tile/track_tile.dart | 54 +++++--- lib/modules/player/player_queue.dart | 126 ++++++++++++++++++- lib/modules/player/player_queue_actions.dart | 44 +++++++ lib/pages/settings/blacklist.dart | 2 +- 5 files changed, 207 insertions(+), 20 deletions(-) create mode 100644 lib/modules/player/player_queue_actions.dart diff --git a/.gitignore b/.gitignore index 119e42e5..544dbba8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ + # IntelliJ related *.iml *.ipr diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 955ac90d..ec3f50f3 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -39,6 +39,7 @@ class TrackTile extends HookConsumerWidget { final int? index; final SpotubeTrackObject track; final bool selected; + final bool selectionMode; final ValueChanged? onChanged; final Future Function()? onTap; final VoidCallback? onLongPress; @@ -53,6 +54,7 @@ class TrackTile extends HookConsumerWidget { this.index, required this.track, this.selected = false, + this.selectionMode = false, required this.playlist, this.onTap, this.onLongPress, @@ -81,6 +83,12 @@ class TrackTile extends HookConsumerWidget { [track.album.images], ); + // Treat either explicit selectionMode or presence of onChanged as selection + // context. Some lists enable selection by providing `onChanged` without + // toggling a dedicated `selectionMode` flag (e.g. playlists), so we must + // disable inner navigation in both cases. + final effectiveSelection = selectionMode || onChanged != null; + return LayoutBuilder(builder: (context, constrains) { return Listener( onPointerDown: (event) { @@ -222,7 +230,9 @@ class TrackTile extends HookConsumerWidget { children: [ Expanded( flex: 6, - child: switch (track) { + child: AbsorbPointer( + absorbing: selectionMode, + child: switch (track) { SpotubeLocalTrackObject() => Text( track.name, maxLines: 1, @@ -232,15 +242,17 @@ class TrackTile extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: Button( - style: ButtonVariance.link.copyWith( - padding: (context, states, value) => - EdgeInsets.zero, - ), - onPressed: () { - context - .navigateTo(TrackRoute(trackId: track.id)); - }, + child: Button( + style: ButtonVariance.link.copyWith( + padding: (context, states, value) => + EdgeInsets.zero, + ), + onPressed: effectiveSelection + ? null + : () { + context + .navigateTo(TrackRoute(trackId: track.id)); + }, child: Text( track.name, maxLines: 1, @@ -251,6 +263,7 @@ class TrackTile extends HookConsumerWidget { ], ), }, + ), ), if (constrains.mdAndUp) ...[ const SizedBox(width: 8), @@ -281,20 +294,25 @@ class TrackTile extends HookConsumerWidget { ), subtitle: Align( alignment: Alignment.centerLeft, - child: track is SpotubeLocalTrackObject + child: track is SpotubeLocalTrackObject ? Text( track.artists.asString(), ) : ClipRect( child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 40), - child: ArtistLink( - artists: track.artists, - onOverflowArtistClick: () { - context.navigateTo( - TrackRoute(trackId: track.id), - ); - }, + child: AbsorbPointer( + absorbing: effectiveSelection, + child: ArtistLink( + artists: track.artists, + onOverflowArtistClick: effectiveSelection + ? () {} + : () { + context.navigateTo( + TrackRoute(trackId: track.id), + ); + }, + ), ), ), ), diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index c9d5626f..bfb7a2e3 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -9,13 +9,16 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/button/back_button.dart'; +import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/track_tile/track_tile.dart'; +import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/modules/player/player_queue_actions.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; @@ -55,6 +58,9 @@ class PlayerQueue extends HookConsumerWidget { final controller = useAutoScrollController(); final searchText = useState(''); + final selectionMode = useState(false); + final selectedTrackIds = useState({}); + final isSearching = useState(false); final tracks = playlist.tracks; @@ -131,6 +137,91 @@ class PlayerQueue extends HookConsumerWidget { surfaceOpacity: 0, child: searchBar, ) + else if (selectionMode.value) + AppBar( + backgroundColor: Colors.transparent, + surfaceBlur: 0, + surfaceOpacity: 0, + leading: [ + IconButton.ghost( + icon: const Icon(SpotubeIcons.close), + onPressed: () { + selectedTrackIds.value = {}; + selectionMode.value = false; + }, + ) + ], + title: SizedBox( + height: 30, + child: AutoSizeText( + '${selectedTrackIds.value.length} selected', + maxLines: 1, + ), + ), + trailing: [ + PlayerQueueActionButton( + builder: (context, close) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Gap(12), + ButtonTile( + style: const ButtonStyle.ghost(), + leading: + const Icon(SpotubeIcons.selectionCheck), + title: Text(context.l10n.select_all), + onPressed: () { + selectedTrackIds.value = + filteredTracks.map((t) => t.id).toSet(); + Navigator.pop(context); + }, + ), + ButtonTile( + style: const ButtonStyle.ghost(), + leading: const Icon(SpotubeIcons.playlistAdd), + title: Text(context.l10n.add_to_playlist), + onPressed: () async { + final selected = filteredTracks + .where((t) => + selectedTrackIds.value.contains(t.id)) + .toList(); + close(); + if (selected.isEmpty) return; + final res = await showDialog( + context: context, + builder: (context) => + PlaylistAddTrackDialog( + tracks: selected, + openFromPlaylist: null, + ), + ); + if (res == true) { + selectedTrackIds.value = {}; + selectionMode.value = false; + } + }, + ), + ButtonTile( + style: const ButtonStyle.ghost(), + leading: const Icon(SpotubeIcons.trash), + title: Text(context.l10n.remove_from_queue), + onPressed: () async { + final ids = selectedTrackIds.value.toList(); + close(); + if (ids.isEmpty) return; + await Future.wait( + ids.map((id) => onRemove(id))); + if (context.mounted) { + selectedTrackIds.value = {}; + selectionMode.value = false; + } + }, + ), + const Gap(12), + ], + ), + ), + ], + ) else AppBar( trailingGap: 0, @@ -195,6 +286,20 @@ class PlayerQueue extends HookConsumerWidget { }, itemBuilder: (context, i) { final track = filteredTracks.elementAt(i); + + void toggleSelection(String id) { + final s = {...selectedTrackIds.value}; + if (s.contains(id)) { + s.remove(id); + } else { + s.add(id); + } + selectedTrackIds.value = s; + if (selectedTrackIds.value.isEmpty) { + selectionMode.value = false; + } + } + return AutoScrollTag( key: ValueKey(i), controller: controller, @@ -203,15 +308,34 @@ class PlayerQueue extends HookConsumerWidget { playlist: playlist, index: i, track: track, + selectionMode: selectionMode.value, + selected: + selectedTrackIds.value.contains(track.id), + onChanged: selectionMode.value + ? (_) => toggleSelection(track.id) + : null, onTap: () async { + if (selectionMode.value) { + toggleSelection(track.id); + return; + } if (playlist.activeTrack?.id == track.id) { return; } await onJump(track); }, + onLongPress: () { + if (!selectionMode.value) { + selectionMode.value = true; + selectedTrackIds.value = {track.id}; + } else { + toggleSelection(track.id); + } + }, leadingActions: [ if (!isSearching.value && - searchText.value.isEmpty) + searchText.value.isEmpty && + !selectionMode.value) Padding( padding: const EdgeInsets.only(left: 8.0), diff --git a/lib/modules/player/player_queue_actions.dart b/lib/modules/player/player_queue_actions.dart new file mode 100644 index 00000000..3d1666c2 --- /dev/null +++ b/lib/modules/player/player_queue_actions.dart @@ -0,0 +1,44 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/constrains.dart'; + +class PlayerQueueActionButton extends StatelessWidget { + final Widget Function(BuildContext context, VoidCallback close) builder; + + const PlayerQueueActionButton({ + super.key, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return IconButton.ghost( + onPressed: () { + final mediaQuery = MediaQuery.sizeOf(context); + + if (mediaQuery.lgAndUp) { + showDropdown( + context: context, + builder: (context) { + return SizedBox( + width: 220 * context.theme.scaling, + child: Card( + padding: EdgeInsets.zero, + child: builder(context, () => closeOverlay(context)), + ), + ); + }, + ); + } else { + openSheet( + context: context, + builder: (context) => builder(context, () => closeSheet(context)), + position: OverlayPosition.bottom, + ); + } + }, + icon: const Icon(SpotubeIcons.moreHorizontal), + ); + } +} diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 8ac2c1b9..2af899f3 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -64,7 +64,7 @@ class BlackListPage extends HookConsumerWidget { child: TextField( onChanged: (value) => searchText.value = value, placeholder: Text(context.l10n.search), - leading: const Icon(SpotubeIcons.search), + // prefixIcon: const Icon(SpotubeIcons.search), ), ), InterScrollbar( From 834445eda3aba3c5329a1d61abc2b334895b1b4b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 10 Nov 2025 13:07:58 +0600 Subject: [PATCH 31/47] chore: remove jsf as arm doesn't build --- .../youtube_engine/quickjs_solver.dart | 282 +++++++++--------- linux/flutter/generated_plugin_registrant.cc | 8 - linux/flutter/generated_plugins.cmake | 3 - macos/Flutter/GeneratedPluginRegistrant.swift | 4 - pubspec.lock | 66 +--- pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 6 - windows/flutter/generated_plugins.cmake | 3 - 8 files changed, 147 insertions(+), 228 deletions(-) diff --git a/lib/services/youtube_engine/quickjs_solver.dart b/lib/services/youtube_engine/quickjs_solver.dart index 4e8bfafb..a7c032f6 100644 --- a/lib/services/youtube_engine/quickjs_solver.dart +++ b/lib/services/youtube_engine/quickjs_solver.dart @@ -1,167 +1,167 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:youtube_explode_dart/js_challenge.dart'; -// ignore: implementation_imports -import 'package:youtube_explode_dart/src/reverse_engineering/challenges/ejs/ejs.dart'; -import 'package:jsf/jsf.dart'; +// import 'dart:async'; +// import 'dart:collection'; +// import 'dart:convert'; +// import 'package:flutter/foundation.dart'; +// import 'package:http/http.dart' as http; +// import 'package:youtube_explode_dart/js_challenge.dart'; +// // ignore: implementation_imports +// import 'package:youtube_explode_dart/src/reverse_engineering/challenges/ejs/ejs.dart'; +// import 'package:jsf/jsf.dart'; -/// [WIP] -class QuickJSEJSSolver extends BaseJSChallengeSolver { - final _playerCache = {}; - final _sigCache = <(String, String, JSChallengeType), String>{}; - final QuickJSRuntime qjs; - QuickJSEJSSolver._(this.qjs); +// /// [WIP] +// class QuickJSEJSSolver extends BaseJSChallengeSolver { +// final _playerCache = {}; +// final _sigCache = <(String, String, JSChallengeType), String>{}; +// final QuickJSRuntime qjs; +// QuickJSEJSSolver._(this.qjs); - static Future init() async { - final modules = await EJSBuilder.getJSModules(); - final deno = await QuickJSRuntime.init(modules); - return QuickJSEJSSolver._(deno); - } +// static Future init() async { +// final modules = await EJSBuilder.getJSModules(); +// final deno = await QuickJSRuntime.init(modules); +// return QuickJSEJSSolver._(deno); +// } - @override - Future solve( - String playerUrl, JSChallengeType type, String challenge) async { - final key = (playerUrl, challenge, type); - if (_sigCache.containsKey(key)) { - return _sigCache[key]!; - } +// @override +// Future solve( +// String playerUrl, JSChallengeType type, String challenge) async { +// final key = (playerUrl, challenge, type); +// if (_sigCache.containsKey(key)) { +// return _sigCache[key]!; +// } - var playerScript = _playerCache[playerUrl]; - if (playerScript == null) { - final resp = await http.get(Uri.parse(playerUrl)); - playerScript = _playerCache[playerUrl] = resp.body; - } - final jsCall = EJSBuilder.buildJSCall(playerScript, { - type: [challenge], - }); +// var playerScript = _playerCache[playerUrl]; +// if (playerScript == null) { +// final resp = await http.get(Uri.parse(playerUrl)); +// playerScript = _playerCache[playerUrl] = resp.body; +// } +// final jsCall = EJSBuilder.buildJSCall(playerScript, { +// type: [challenge], +// }); - final result = await qjs.eval(jsCall); - // Trim the first and last characters (' delimiters of the JS string) - final data = json.decode(result.substring(1, result.length - 1)) - as Map; +// final result = await qjs.eval(jsCall); +// // Trim the first and last characters (' delimiters of the JS string) +// final data = json.decode(result.substring(1, result.length - 1)) +// as Map; - if (data['type'] != 'result') { - throw Exception('Unexpected response type: ${data['type']}'); - } - final response = data['responses'][0]; - if (response['type'] != 'result') { - throw Exception('Unexpected item response type: ${response['type']}'); - } - final decoded = response['data'][challenge]; - if (decoded == null) { - throw Exception('No data for challenge: $challenge'); - } +// if (data['type'] != 'result') { +// throw Exception('Unexpected response type: ${data['type']}'); +// } +// final response = data['responses'][0]; +// if (response['type'] != 'result') { +// throw Exception('Unexpected item response type: ${response['type']}'); +// } +// final decoded = response['data'][challenge]; +// if (decoded == null) { +// throw Exception('No data for challenge: $challenge'); +// } - _sigCache[key] = decoded; +// _sigCache[key] = decoded; - return decoded; - } +// return decoded; +// } - @override - void dispose() { - qjs.dispose(); - } -} +// @override +// void dispose() { +// qjs.dispose(); +// } +// } -class _EvalRequest { - final String code; - final Completer completer; +// class _EvalRequest { +// final String code; +// final Completer completer; - _EvalRequest(this.code, this.completer); -} +// _EvalRequest(this.code, this.completer); +// } -class QuickJSRuntime { - final JsRuntime _runtime; - final StreamController _stdoutController = - StreamController.broadcast(); +// class QuickJSRuntime { +// final JsRuntime _runtime; +// final StreamController _stdoutController = +// StreamController.broadcast(); - // Queue for incoming eval requests - final Queue<_EvalRequest> _evalQueue = Queue<_EvalRequest>(); - bool _isProcessing = false; // Flag to indicate if an eval is currently active +// // Queue for incoming eval requests +// final Queue<_EvalRequest> _evalQueue = Queue<_EvalRequest>(); +// bool _isProcessing = false; // Flag to indicate if an eval is currently active - QuickJSRuntime(this._runtime); +// QuickJSRuntime(this._runtime); - /// Disposes the Deno process. - void dispose() { - _stdoutController.close(); - _runtime.dispose(); - } +// /// Disposes the Deno process. +// void dispose() { +// _stdoutController.close(); +// _runtime.dispose(); +// } - /// Sends JavaScript code to Deno for evaluation. - /// Assumes single-line input produces single-line output. - Future eval(String code) { - final completer = Completer(); - final request = _EvalRequest(code, completer); - _evalQueue.addLast(request); // Add request to the end of the queue - _processQueue(); // Attempt to process the queue +// /// Sends JavaScript code to Deno for evaluation. +// /// Assumes single-line input produces single-line output. +// Future eval(String code) { +// final completer = Completer(); +// final request = _EvalRequest(code, completer); +// _evalQueue.addLast(request); // Add request to the end of the queue +// _processQueue(); // Attempt to process the queue - return completer.future; - } +// return completer.future; +// } - // Processes the eval queue. - void _processQueue() { - if (_isProcessing || _evalQueue.isEmpty) { - return; // Already processing or nothing in queue - } +// // Processes the eval queue. +// void _processQueue() { +// if (_isProcessing || _evalQueue.isEmpty) { +// return; // Already processing or nothing in queue +// } - _isProcessing = true; - final request = - _evalQueue.first; // Get the next request without removing it yet +// _isProcessing = true; +// final request = +// _evalQueue.first; // Get the next request without removing it yet - StreamSubscription? currentOutputSubscription; - Completer lineReceived = Completer(); +// StreamSubscription? currentOutputSubscription; +// Completer lineReceived = Completer(); - currentOutputSubscription = _stdoutController.stream.listen((data) { - if (!lineReceived.isCompleted) { - // Assuming single line output per eval. - // This will capture the first full line or chunk received after sending the code. - request.completer.complete(data.trim()); - lineReceived.complete(); - currentOutputSubscription - ?.cancel(); // Cancel subscription for this request - _evalQueue.removeFirst(); // Remove the processed request - _isProcessing = false; // Mark as no longer processing - _processQueue(); // Attempt to process next item in queue - } - }, onError: (e) { - if (!request.completer.isCompleted) { - request.completer.completeError(e); - lineReceived.completeError(e); - currentOutputSubscription?.cancel(); - _evalQueue.removeFirst(); - _isProcessing = false; - _processQueue(); - } - }, onDone: () { - if (!request.completer.isCompleted) { - request.completer.completeError( - StateError('Deno process closed while awaiting eval result.')); - lineReceived.completeError( - StateError('Deno process closed while awaiting eval result.')); - currentOutputSubscription?.cancel(); - _evalQueue.removeFirst(); - _isProcessing = false; - _processQueue(); - } - }); +// currentOutputSubscription = _stdoutController.stream.listen((data) { +// if (!lineReceived.isCompleted) { +// // Assuming single line output per eval. +// // This will capture the first full line or chunk received after sending the code. +// request.completer.complete(data.trim()); +// lineReceived.complete(); +// currentOutputSubscription +// ?.cancel(); // Cancel subscription for this request +// _evalQueue.removeFirst(); // Remove the processed request +// _isProcessing = false; // Mark as no longer processing +// _processQueue(); // Attempt to process next item in queue +// } +// }, onError: (e) { +// if (!request.completer.isCompleted) { +// request.completer.completeError(e); +// lineReceived.completeError(e); +// currentOutputSubscription?.cancel(); +// _evalQueue.removeFirst(); +// _isProcessing = false; +// _processQueue(); +// } +// }, onDone: () { +// if (!request.completer.isCompleted) { +// request.completer.completeError( +// StateError('Deno process closed while awaiting eval result.')); +// lineReceived.completeError( +// StateError('Deno process closed while awaiting eval result.')); +// currentOutputSubscription?.cancel(); +// _evalQueue.removeFirst(); +// _isProcessing = false; +// _processQueue(); +// } +// }); - debugPrint("[QuickJS Solver] Evaluate ${request.code}"); - final result = _runtime.eval(request.code); - debugPrint("[QuickJS Solver] Evaluation Result $result"); - _stdoutController.add(result); - } +// debugPrint("[QuickJS Solver] Evaluate ${request.code}"); +// final result = _runtime.eval(request.code); +// debugPrint("[QuickJS Solver] Evaluation Result $result"); +// _stdoutController.add(result); +// } - static Future init(String initCode) async { - debugPrint("[QuickJS Solver] Initializing"); - debugPrint("[QuickJS Solver] script $initCode"); +// static Future init(String initCode) async { +// debugPrint("[QuickJS Solver] Initializing"); +// debugPrint("[QuickJS Solver] script $initCode"); - final runtime = JsRuntime(); +// final runtime = JsRuntime(); - runtime.execInitScript(initCode); +// runtime.execInitScript(initCode); - return QuickJSRuntime(runtime); - } -} +// return QuickJSRuntime(runtime); +// } +// } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 63e83265..8f5a71fe 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -12,13 +12,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -43,9 +41,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); - g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); - irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); g_autoptr(FlPluginRegistrar) local_notifier_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin"); local_notifier_plugin_register_with_registrar(local_notifier_registrar); @@ -61,9 +56,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); - super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) system_theme_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); system_theme_plugin_register_with_registrar(system_theme_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 541826e6..1dd92b5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -9,13 +9,11 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux flutter_timezone gtk - irondash_engine_context local_notifier media_kit_libs_linux open_file_linux screen_retriever_linux sqlite3_flutter_libs - super_native_extensions system_theme tray_manager url_launcher_linux @@ -24,7 +22,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc - jsf metadata_god ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d211f518..2931c1b4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,7 +18,6 @@ import flutter_inappwebview_macos import flutter_new_pipe_extractor import flutter_secure_storage_macos import flutter_timezone -import irondash_engine_context import local_notifier import media_kit_libs_macos_audio import open_file_mac @@ -28,7 +27,6 @@ import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin import sqlite3_flutter_libs -import super_native_extensions import system_theme import tray_manager import url_launcher_macos @@ -48,7 +46,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterNewPipeExtractorPlugin.register(with: registry.registrar(forPlugin: "FlutterNewPipeExtractorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) - IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) @@ -58,7 +55,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) - SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7e53e91c..33275111 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -422,10 +422,10 @@ packages: dependency: transitive description: name: country_flags - sha256: "78a7bf8aabd7ae1a90087f0c517471ac9ebfe07addc652692f58da0f0f833196" + sha256: "714f2d415e74828eb08787d552a05e94cdf2cbe0607a5656f3e70087cd7bb7e0" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.1.0" coverage: dependency: transitive description: @@ -1399,22 +1399,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - irondash_engine_context: - dependency: transitive - description: - name: irondash_engine_context - sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" - url: "https://pub.dev" - source: hosted - version: "0.5.5" - irondash_message_channel: - dependency: transitive - description: - name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 - url: "https://pub.dev" - source: hosted - version: "0.7.0" jovial_misc: dependency: transitive description: @@ -1439,14 +1423,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - jsf: - dependency: "direct main" - description: - name: jsf - sha256: "189ba3b9216702f9b6f2d8ea90fa5acaca13bbe5dd2f72fb38618005b41a737f" - url: "https://pub.dev" - source: hosted - version: "0.6.1" json_annotation: dependency: "direct main" description: @@ -1910,14 +1886,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.4" - pixel_snap: - dependency: transitive - description: - name: pixel_snap - sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" - url: "https://pub.dev" - source: hosted - version: "0.1.5" platform: dependency: transitive description: @@ -2107,10 +2075,10 @@ packages: dependency: "direct main" description: name: shadcn_flutter - sha256: af83de199b7c3a965ab24e293cfcafe2764c12b7f911f5b1a427c332029262d9 + sha256: "1fd4f798c39d6308dc8f7e94d9e870b5db39fbf417ea95c423c7555ce8227a1c" url: "https://pub.dev" source: hosted - version: "0.0.44" + version: "0.0.47" shared_preferences: dependency: "direct main" description: @@ -2428,22 +2396,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" - super_clipboard: - dependency: transitive - description: - name: super_clipboard - sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - super_native_extensions: - dependency: transitive - description: - name: super_native_extensions - sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 - url: "https://pub.dev" - source: hosted - version: "0.9.1" sync_http: dependency: transitive description: @@ -2460,14 +2412,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" - syntax_highlight: - dependency: transitive - description: - name: syntax_highlight - sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138" - url: "https://pub.dev" - source: hosted - version: "0.5.0" system_theme: dependency: "direct main" description: @@ -2863,4 +2807,4 @@ packages: version: "1.0.0" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.1" + flutter: ">=3.32.3" diff --git a/pubspec.yaml b/pubspec.yaml index 9b84196b..65cf4576 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -104,7 +104,7 @@ dependencies: ref: dart-3-support url: https://github.com/KRTirtho/scrobblenaut.git scroll_to_index: ^3.0.1 - shadcn_flutter: ^0.0.42 + shadcn_flutter: ^0.0.47 shared_preferences: ^2.2.3 shelf: ^1.4.1 shelf_router: ^1.1.4 @@ -161,7 +161,6 @@ dependencies: flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 change_case: ^1.1.0 - jsf: ^0.6.1 dev_dependencies: build_runner: ^2.4.13 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ac2fd1e0..95f52491 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -15,13 +15,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -46,8 +44,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterTimezonePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); - IrondashEngineContextPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); LocalNotifierPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalNotifierPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( @@ -58,8 +54,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); - SuperNativeExtensionsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); TrayManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 53cd3667..51be79c2 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,13 +12,11 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_new_pipe_extractor flutter_secure_storage_windows flutter_timezone - irondash_engine_context local_notifier media_kit_libs_windows_audio permission_handler_windows screen_retriever_windows sqlite3_flutter_libs - super_native_extensions system_theme tray_manager url_launcher_windows @@ -27,7 +25,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc - jsf metadata_god smtc_windows ) From 4fae9013a7ffa5cd01980e983076dbf8520206db Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 11 Nov 2025 10:39:13 +0600 Subject: [PATCH 32/47] fix: download not working in different devices and slow --- .../dialogs/replace_downloaded_dialog.dart | 3 +- .../presentation_actions.dart | 2 +- lib/components/track_tile/track_options.dart | 23 +- lib/extensions/dio.dart | 168 ++++++ .../library/user_downloads/download_item.dart | 133 ++--- lib/modules/player/player_actions.dart | 8 +- lib/modules/root/sidebar/sidebar_footer.dart | 7 +- lib/modules/root/spotube_navigation_bar.dart | 7 +- lib/modules/root/use_downloader_dialogs.dart | 46 -- lib/pages/library/library.dart | 7 +- lib/pages/library/user_downloads.dart | 19 +- .../user_local_tracks/local_folder.dart | 127 ++++- .../user_local_tracks/user_local_tracks.dart | 1 - lib/pages/root/root_app.dart | 2 - lib/provider/audio_player/state.dart | 6 +- lib/provider/download_manager_provider.dart | 517 +++++++++--------- .../track_options/track_options_provider.dart | 19 +- .../download_manager/chunked_download.dart | 145 ----- .../download_manager/download_manager.dart | 416 -------------- .../download_manager/download_request.dart | 24 - .../download_manager/download_status.dart | 26 - .../download_manager/download_task.dart | 35 -- 22 files changed, 653 insertions(+), 1088 deletions(-) create mode 100644 lib/extensions/dio.dart delete mode 100644 lib/modules/root/use_downloader_dialogs.dart delete mode 100644 lib/services/download_manager/chunked_download.dart delete mode 100644 lib/services/download_manager/download_manager.dart delete mode 100644 lib/services/download_manager/download_request.dart delete mode 100644 lib/services/download_manager/download_status.dart delete mode 100644 lib/services/download_manager/download_task.dart diff --git a/lib/components/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart index 6634a039..5b5b194e 100644 --- a/lib/components/dialogs/replace_downloaded_dialog.dart +++ b/lib/components/dialogs/replace_downloaded_dialog.dart @@ -12,13 +12,12 @@ class ReplaceDownloadedDialog extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final groupValue = ref.watch(replaceDownloadedFileState); final replaceAll = ref.watch(replaceDownloadedFileState); return AlertDialog( title: Text(context.l10n.track_exists(track.name)), content: RadioGroup( - value: groupValue, + value: replaceAll, onChanged: (value) { ref.read(replaceDownloadedFileState.notifier).state = value; }, diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart index 54aa3428..61202a48 100644 --- a/lib/components/track_presentation/presentation_actions.dart +++ b/lib/components/track_presentation/presentation_actions.dart @@ -89,7 +89,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget { ) ?? false; if (confirmed != true) return; - downloader.batchAddToQueue(fullTrackObjects); + downloader.addAllToQueue(fullTrackObjects); notifier.deselectAllTracks(); if (!context.mounted) return; showToastForAction(context, action, fullTrackObjects.length); diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 61b47968..7d14493e 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -1,5 +1,3 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; - import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -44,7 +42,7 @@ class TrackOptions extends HookConsumerWidget { :isActiveTrack, :isAuthenticated, :isLiked, - :progressNotifier + :downloadTask ) = ref.watch(trackOptionsStateProvider(track)); final isLocalTrack = track is SpotubeLocalTrackObject; @@ -211,12 +209,19 @@ class TrackOptions extends HookConsumerWidget { }, enabled: !isInDownloadQueue, leading: isInDownloadQueue - ? HookBuilder(builder: (context) { - final progress = useListenable(progressNotifier); - return CircularProgressIndicator( - value: progress?.value, - ); - }) + ? StreamBuilder( + stream: downloadTask?.downloadedBytesStream, + builder: (context, snapshot) { + final progress = downloadTask?.totalSizeBytes == null || + downloadTask?.totalSizeBytes == 0 + ? 0 + : (snapshot.data ?? 0) / + downloadTask!.totalSizeBytes!; + return CircularProgressIndicator( + value: progress.toDouble(), + ); + }, + ) : const Icon(SpotubeIcons.download), title: Text(context.l10n.download_track), ), diff --git a/lib/extensions/dio.dart b/lib/extensions/dio.dart new file mode 100644 index 00000000..81bb1e70 --- /dev/null +++ b/lib/extensions/dio.dart @@ -0,0 +1,168 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +extension ChunkDownloaderDioExtension on Dio { + Future chunkDownload( + String urlPath, + dynamic savePath, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + bool deleteOnError = true, + FileAccessMode fileAccessMode = FileAccessMode.write, + String lengthHeader = Headers.contentLengthHeader, + Object? data, + Options? options, + int connections = 4, + }) async { + final targetFile = File(savePath.toString()); + final tempRootDir = await getTemporaryDirectory(); + final tempSaveDir = Directory( + join( + tempRootDir.path, + 'Spotube', + '.chunk_dl_${targetFile.uri.pathSegments.last}', + ), + ); + if (await tempSaveDir.exists()) await tempSaveDir.delete(recursive: true); + await tempSaveDir.create(recursive: true); + + try { + int? totalLength; + bool supportsRange = false; + + Response? headResp; + try { + headResp = await head( + urlPath, + queryParameters: queryParameters, + options: Options( + headers: {'Range': 'bytes=0-0'}, + followRedirects: true, + ), + ); + } catch (_) { + // Some servers reject HEAD -> ignore + } + + final lengthStr = headResp?.headers[lengthHeader]?.first; + if (lengthStr != null) { + final parsed = int.tryParse(lengthStr); + if (parsed != null && parsed > 1) { + totalLength = parsed; + } + } + + supportsRange = headResp?.statusCode == 206 || + headResp?.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes'; + + if (totalLength == null || totalLength <= 1) { + final resp = await get( + urlPath, + options: Options( + responseType: ResponseType.stream, + ), + queryParameters: queryParameters, + cancelToken: cancelToken, + ); + + final len = int.tryParse(resp.headers[lengthHeader]?.first ?? ''); + if (len == null || len <= 1) { + // can’t safely chunk — fallback + return download( + urlPath, + savePath, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + deleteOnError: deleteOnError, + options: options, + data: data, + ); + } + + totalLength = len; + supportsRange = + resp.headers.value(HttpHeaders.acceptRangesHeader)?.toLowerCase() == + 'bytes'; + } + + if (!supportsRange || connections <= 1) { + return download( + urlPath, + savePath, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + deleteOnError: deleteOnError, + options: options, + data: data, + ); + } + + final chunkSize = (totalLength / connections).ceil(); + int downloaded = 0; + + final partFiles = List.generate( + connections, + (i) => File(join(tempSaveDir.path, 'part_$i')), + ); + + final futures = List.generate(connections, (i) async { + final start = i * chunkSize; + final end = (i + 1) * chunkSize - 1; + if (start >= totalLength!) return; + + final resp = await get( + urlPath, + options: Options( + responseType: ResponseType.stream, + headers: {'Range': 'bytes=$start-$end'}, + ), + queryParameters: queryParameters, + cancelToken: cancelToken, + ); + + final file = partFiles[i]; + if (await file.exists()) await file.delete(); + await file.create(recursive: true); + final sink = file.openWrite(); + + await for (final chunk in resp.data!.stream) { + sink.add(chunk); + downloaded += chunk.length; + onReceiveProgress?.call(downloaded, totalLength); + } + + await sink.close(); + }); + + await Future.wait(futures); + + final targetSink = targetFile.openWrite(); + for (final f in partFiles) { + await targetSink.addStream(f.openRead()); + } + await targetSink.close(); + + await tempSaveDir.delete(recursive: true); + + return Response( + requestOptions: RequestOptions(path: urlPath), + data: targetFile, + statusCode: 200, + statusMessage: 'Chunked download completed ($connections connections)', + ); + } catch (e) { + if (deleteOnError) { + if (await targetFile.exists()) await targetFile.delete(); + if (await tempSaveDir.exists()) { + await tempSaveDir.delete(recursive: true); + } + } + rethrow; + } + } +} diff --git a/lib/modules/library/user_downloads/download_item.dart b/lib/modules/library/user_downloads/download_item.dart index 2dcfc28f..b1cd9f62 100644 --- a/lib/modules/library/user_downloads/download_item.dart +++ b/lib/modules/library/user_downloads/download_item.dart @@ -7,44 +7,19 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/ui/button_tile.dart'; -import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/services/download_manager/download_status.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; class DownloadItem extends HookConsumerWidget { - final SpotubeFullTrackObject track; + final DownloadTask task; const DownloadItem({ super.key, - required this.track, + required this.task, }); @override Widget build(BuildContext context, ref) { - final downloadManager = ref.watch(downloadManagerProvider); - - final taskStatus = useState(null); - - useEffect(() { - if (track is! SourcedTrack) return null; - final notifier = downloadManager.getStatusNotifier(track); - - taskStatus.value = notifier?.value; - - void listener() { - taskStatus.value = notifier?.value; - } - - notifier?.addListener(listener); - - return () { - notifier?.removeListener(listener); - }; - }, [track]); - - final isQueryingSourceInfo = - taskStatus.value == null || track is! SourcedTrack; + final downloadManager = ref.watch(downloadManagerProvider.notifier); return ButtonTile( style: ButtonVariance.ghost, @@ -55,90 +30,72 @@ class DownloadItem extends HookConsumerWidget { child: UniversalImage( height: 40, width: 40, - path: track.album.images.asUrlString( + path: task.track.album.images.asUrlString( placeholder: ImagePlaceholder.albumArt, ), ), ), ), - title: Text(track.name), + title: Text(task.track.name), subtitle: ArtistLink( - artists: track.artists, + artists: task.track.artists, mainAxisAlignment: WrapAlignment.start, onOverflowArtistClick: () { - context.navigateTo(TrackRoute(trackId: track.id)); + context.navigateTo(TrackRoute(trackId: task.track.id)); }, ), - trailing: isQueryingSourceInfo - ? Text(context.l10n.querying_info).small() - : switch (taskStatus.value!) { - DownloadStatus.downloading => HookBuilder(builder: (context) { - final taskProgress = useListenable(useMemoized( - () => downloadManager.getProgressNotifier(track), - [track], - )); + trailing: switch (task.status) { + DownloadStatus.downloading => HookBuilder(builder: (context) { + return StreamBuilder( + stream: task.downloadedBytesStream, + builder: (context, asyncSnapshot) { + final progress = + task.totalSizeBytes == null || task.totalSizeBytes == 0 + ? 0 + : (asyncSnapshot.data ?? 0) / task.totalSizeBytes!; + return Row( children: [ CircularProgressIndicator( - value: taskProgress?.value ?? 0, + value: progress.toDouble(), ), const SizedBox(width: 10), - IconButton.ghost( - icon: const Icon(SpotubeIcons.pause), - onPressed: () { - downloadManager.pause(track); - }), const SizedBox(width: 10), IconButton.ghost( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.cancel(track); + downloadManager.cancel(task.track); }), ], ); - }), - DownloadStatus.paused => Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton.ghost( - icon: const Icon(SpotubeIcons.play), - onPressed: () { - downloadManager.resume(track); - }), - const SizedBox(width: 10), - IconButton.ghost( - icon: const Icon(SpotubeIcons.close), - onPressed: () { - downloadManager.cancel(track); - }) - ], + }); + }), + DownloadStatus.failed || DownloadStatus.canceled => SizedBox( + width: 100, + child: Row( + children: [ + Icon( + SpotubeIcons.error, + color: Colors.red[400], ), - DownloadStatus.failed || DownloadStatus.canceled => SizedBox( - width: 100, - child: Row( - children: [ - Icon( - SpotubeIcons.error, - color: Colors.red[400], - ), - const SizedBox(width: 10), - IconButton.ghost( - icon: const Icon(SpotubeIcons.refresh), - onPressed: () { - downloadManager.retry(track); - }, - ), - ], - ), - ), - DownloadStatus.completed => - Icon(SpotubeIcons.done, color: Colors.green[400]), - DownloadStatus.queued => IconButton.ghost( - icon: const Icon(SpotubeIcons.close), + const SizedBox(width: 10), + IconButton.ghost( + icon: const Icon(SpotubeIcons.refresh), onPressed: () { - downloadManager.removeFromQueue(track); - }), - }, + downloadManager.retry(task.track); + }, + ), + ], + ), + ), + DownloadStatus.completed => + Icon(SpotubeIcons.done, color: Colors.green[400]), + DownloadStatus.queued => IconButton.ghost( + icon: const Icon(SpotubeIcons.close), + onPressed: () { + downloadManager.cancel(task.track); + }), + }, ); } } diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index df1e2a2d..9f8639ec 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -43,8 +43,12 @@ class PlayerActions extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider.notifier); final isInQueue = useMemoized(() { if (playlist.activeTrack is! SpotubeFullTrackObject) return false; - return downloader - .isActive(playlist.activeTrack! as SpotubeFullTrackObject); + final downloadTask = + downloader.getTaskByTrackId(playlist.activeTrack!.id); + return const [ + DownloadStatus.queued, + DownloadStatus.downloading, + ].contains(downloadTask?.status); }, [ playlist.activeTrack, downloader, diff --git a/lib/modules/root/sidebar/sidebar_footer.dart b/lib/modules/root/sidebar/sidebar_footer.dart index 4c46c13b..0f8ac9d8 100644 --- a/lib/modules/root/sidebar/sidebar_footer.dart +++ b/lib/modules/root/sidebar/sidebar_footer.dart @@ -24,7 +24,12 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem { final theme = Theme.of(context); final router = AutoRouter.of(context, watch: true); final mediaQuery = MediaQuery.of(context); - final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; + final downloadCount = ref + .watch(downloadManagerProvider) + .where((e) => + e.status == DownloadStatus.downloading || + e.status == DownloadStatus.queued) + .length; final userSnapshot = ref.watch(metadataPluginUserProvider); final data = userSnapshot.asData?.value; diff --git a/lib/modules/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart index 15417fa6..47ea3ca3 100644 --- a/lib/modules/root/spotube_navigation_bar.dart +++ b/lib/modules/root/spotube_navigation_bar.dart @@ -25,7 +25,12 @@ class SpotubeNavigationBar extends HookConsumerWidget { Widget build(BuildContext context, ref) { final mediaQuery = MediaQuery.of(context); - final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; + final downloadCount = ref + .watch(downloadManagerProvider) + .where((e) => + e.status == DownloadStatus.downloading || + e.status == DownloadStatus.queued) + .length; final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); diff --git a/lib/modules/root/use_downloader_dialogs.dart b/lib/modules/root/use_downloader_dialogs.dart deleted file mode 100644 index e2f91043..00000000 --- a/lib/modules/root/use_downloader_dialogs.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; -import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; -import 'package:spotube/provider/download_manager_provider.dart'; - -void useDownloaderDialogs(WidgetRef ref) { - final context = useContext(); - final showingDialogCompleter = useRef(Completer()..complete()); - final downloader = ref.watch(downloadManagerProvider); - - useEffect(() { - downloader.onFileExists = (track) async { - if (!context.mounted) return false; - - if (!showingDialogCompleter.value.isCompleted) { - await showingDialogCompleter.value.future; - } - - final replaceAll = ref.read(replaceDownloadedFileState); - - if (replaceAll != null) return replaceAll; - - showingDialogCompleter.value = Completer(); - - if (context.mounted) { - final result = await showDialog( - context: context, - builder: (context) => ReplaceDownloadedDialog( - track: track, - ), - ) ?? - false; - - showingDialogCompleter.value.complete(); - return result; - } - - // it'll never reach here as root_app is always mounted - return false; - }; - return null; - }, [downloader]); -} diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index 172d9af3..de438451 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -17,7 +17,12 @@ class LibraryPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount; + final downloadingCount = ref + .watch(downloadManagerProvider) + .where((e) => + e.status == DownloadStatus.downloading || + e.status == DownloadStatus.queued) + .length; final router = context.watchRouter; final sidebarLibraryTileList = useMemoized( () => [ diff --git a/lib/pages/library/user_downloads.dart b/lib/pages/library/user_downloads.dart index 73dc692f..f6a130bb 100644 --- a/lib/pages/library/user_downloads.dart +++ b/lib/pages/library/user_downloads.dart @@ -14,9 +14,8 @@ class UserDownloadsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final downloadManager = ref.watch(downloadManagerProvider); - - final history = downloadManager.$history; + final downloadQueue = ref.watch(downloadManagerProvider); + final downloadManagerNotifier = ref.watch(downloadManagerProvider.notifier); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -28,16 +27,15 @@ class UserDownloadsPage extends HookConsumerWidget { children: [ Expanded( child: AutoSizeText( - context.l10n - .currently_downloading(downloadManager.$downloadCount), + context.l10n.currently_downloading(downloadQueue.length), maxLines: 1, ).semiBold(), ), const SizedBox(width: 10), Button.destructive( - onPressed: downloadManager.$downloadCount == 0 + onPressed: downloadQueue.isEmpty ? null - : downloadManager.cancelAll, + : downloadManagerNotifier.clearAll, child: Text(context.l10n.cancel_all), ), ], @@ -46,9 +44,12 @@ class UserDownloadsPage extends HookConsumerWidget { Expanded( child: SafeArea( child: ListView.builder( - itemCount: history.length, + itemCount: downloadQueue.length, + padding: const EdgeInsets.only(bottom: 200), itemBuilder: (context, index) { - return DownloadItem(track: history.elementAt(index).query); + return DownloadItem( + task: downloadQueue.elementAt(index), + ); }, ), ), diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index 58a7a023..55a148f6 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -14,6 +14,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/button/back_button.dart'; +import 'package:spotube/components/track_presentation/presentation_actions.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; @@ -68,6 +69,37 @@ class LocalLibraryPage extends HookConsumerWidget { } } + Future shufflePlayLocalTracks( + WidgetRef ref, + List tracks, + ) async { + final playlist = ref.read(audioPlayerProvider); + final playback = ref.read(audioPlayerProvider.notifier); + final isPlaylistPlaying = playlist.containsTracks(tracks); + final shuffledTracks = tracks.shuffled(); + if (isPlaylistPlaying) return; + + await playback.load( + shuffledTracks, + initialIndex: 0, + autoPlay: true, + ); + } + + Future addToQueueLocalTracks( + BuildContext context, + WidgetRef ref, + List tracks, + ) async { + final playlist = ref.read(audioPlayerProvider); + final playback = ref.read(audioPlayerProvider.notifier); + final isPlaylistPlaying = playlist.containsTracks(tracks); + if (isPlaylistPlaying) return; + await playback.addTracks(tracks); + if (!context.mounted) return; + showToastForAction(context, "add-to-queue", tracks.length); + } + @override Widget build(BuildContext context, ref) { final scale = context.theme.scaling; @@ -75,8 +107,12 @@ class LocalLibraryPage extends HookConsumerWidget { final sortBy = useState(SortBy.none); final playlist = ref.watch(audioPlayerProvider); final trackSnapshot = ref.watch(localTracksProvider); - final isPlaylistPlaying = playlist.containsTracks( - trackSnapshot.asData?.value.values.flattened.toList() ?? []); + final isPlaylistPlaying = useMemoized( + () => playlist.containsTracks( + trackSnapshot.asData?.value[location] ?? [], + ), + [playlist, trackSnapshot, location], + ); final searchController = useShadcnTextEditingController(); useValueListenable(searchController); @@ -222,26 +258,79 @@ class LocalLibraryPage extends HookConsumerWidget { child: Row( children: [ const Gap(5), - Button.primary( - onPressed: trackSnapshot.asData?.value != null - ? () async { - if (trackSnapshot.asData?.value.isNotEmpty == - true) { - if (!isPlaylistPlaying) { - await playLocalTracks( - ref, - trackSnapshot.asData!.value[location] ?? [], - ); + Tooltip( + tooltip: + TooltipContainer(child: Text(context.l10n.play)).call, + child: IconButton.primary( + onPressed: trackSnapshot.asData?.value != null + ? () async { + if (trackSnapshot.asData?.value.isNotEmpty == + true) { + if (!isPlaylistPlaying) { + await playLocalTracks( + ref, + trackSnapshot.asData!.value[location] ?? + [], + ); + } } } - } - : null, - leading: Icon( - isPlaylistPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, + : null, + icon: Icon( + isPlaylistPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + ), + ), + ), + const Gap(5), + Tooltip( + tooltip: + TooltipContainer(child: Text(context.l10n.shuffle)) + .call, + child: IconButton.outline( + onPressed: trackSnapshot.asData?.value != null + ? () async { + if (trackSnapshot.asData?.value.isNotEmpty == + true) { + if (!isPlaylistPlaying) { + await shufflePlayLocalTracks( + ref, + trackSnapshot.asData!.value[location] ?? + [], + ); + } + } + } + : null, + enabled: !isPlaylistPlaying, + icon: const Icon(SpotubeIcons.shuffle), + ), + ), + const Gap(5), + Tooltip( + tooltip: TooltipContainer( + child: Text(context.l10n.add_to_queue)) + .call, + child: IconButton.outline( + onPressed: trackSnapshot.asData?.value != null + ? () async { + if (trackSnapshot.asData?.value.isNotEmpty == + true) { + if (!isPlaylistPlaying) { + await addToQueueLocalTracks( + context, + ref, + trackSnapshot.asData!.value[location] ?? + [], + ); + } + } + } + : null, + enabled: !isPlaylistPlaying, + icon: const Icon(SpotubeIcons.queueAdd), ), - child: Text(context.l10n.play), ), const Spacer(), if (constraints.smAndDown) diff --git a/lib/pages/library/user_local_tracks/user_local_tracks.dart b/lib/pages/library/user_local_tracks/user_local_tracks.dart index 43fa3cc9..5f7502e6 100644 --- a/lib/pages/library/user_local_tracks/user_local_tracks.dart +++ b/lib/pages/library/user_local_tracks/user_local_tracks.dart @@ -13,7 +13,6 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; -// ignore: depend_on_referenced_packages enum SortBy { none, diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 4cd02881..44b8416f 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -9,7 +9,6 @@ import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/sidebar/sidebar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart'; -import 'package:spotube/modules/root/use_downloader_dialogs.dart'; import 'package:spotube/modules/root/use_global_subscriptions.dart'; import 'package:spotube/provider/glance/glance.dart'; @@ -25,7 +24,6 @@ class RootAppPage extends HookConsumerWidget { ref.listen(glanceProvider, (_, __) {}); useGlobalSubscriptions(ref); - useDownloaderDialogs(ref); useEndlessPlayback(ref); useCheckYtDlpInstalled(ref); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index bb0527bf..7bd47f76 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -51,7 +51,11 @@ class AudioPlayerState with _$AudioPlayerState { } bool containsTrack(SpotubeTrackObject track) { - return tracks.any((t) => t.id == track.id); + return tracks.any( + (t) => t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject + ? t.path == track.path + : t.id == track.id, + ); } bool containsTracks(List tracks) { diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index d64da32e..0ca99ec1 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -1,268 +1,285 @@ import 'dart:async'; import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:metadata_god/metadata_god.dart'; +import 'package:path/path.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' hide join; +import 'package:spotube/collections/routes.dart'; +import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; +import 'package:spotube/extensions/dio.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:metadata_god/metadata_god.dart'; -import 'package:path/path.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/download_manager/download_manager.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/utils/primitive_utils.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/utils/service_utils.dart'; -class DownloadManagerProvider extends ChangeNotifier { - DownloadManagerProvider({required this.ref}) - : $history = {}, - dl = DownloadManager() { - dl.statusStream.listen((event) async { - try { - final (:request, :status) = event; +enum DownloadStatus { + queued, + downloading, + completed, + failed, + canceled, +} - final sourcedTrack = $history.firstWhereOrNull( - (element) => - element.getUrlOfQuality( - downloadContainer, - downloadQualityIndex, - ) == - request.url, - ); +class DownloadTask { + final SpotubeFullTrackObject track; + final DownloadStatus status; + final CancelToken cancelToken; + final int? totalSizeBytes; + final StreamController _downloadedBytesStreamController; - if (sourcedTrack == null) return; + Stream get downloadedBytesStream => + _downloadedBytesStreamController.stream; - final savePath = getTrackFileUrl(sourcedTrack); - // related to onFileExists - final oldFile = File("$savePath.old"); + DownloadTask({ + required this.track, + required this.status, + required this.cancelToken, + this.totalSizeBytes, + StreamController? downloadedBytesStreamController, + }) : _downloadedBytesStreamController = + downloadedBytesStreamController ?? StreamController.broadcast(); - // if download failed and old file exists, rename it back - if ((status == DownloadStatus.failed || - status == DownloadStatus.canceled) && - await oldFile.exists()) { - await oldFile.rename(savePath); - } - - if (status != DownloadStatus.completed || - //? WebA audiotagging is not supported yet - //? Although in future by converting weba to opus & then tagging it - //? is possible using vorbis comments - downloadContainer.getFileExtension() == "weba") { - return; - } - - final file = File(request.path); - - if (await oldFile.exists()) { - await oldFile.delete(); - } - - final imageBytes = await ServiceUtils.downloadImage( - (sourcedTrack.query.album.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - index: 1, - ), - ); - - final metadata = sourcedTrack.query.toMetadata( - fileLength: await file.length(), - imageBytes: imageBytes, - ); - - await MetadataGod.writeMetadata( - file: file.path, - metadata: metadata, - ); - } catch (e, stack) { - AppLogger.reportError(e, stack); - } - }); - } - - Future Function(SpotubeFullTrackObject track) onFileExists = - (SpotubeFullTrackObject track) async => true; - - final Ref ref; - - String get downloadDirectory => - ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); - SpotubeAudioSourceContainerPreset get downloadContainer => ref.read( - audioSourcePresetsProvider - .select((s) => s.presets[s.selectedDownloadingContainerIndex]), - ); - - int get downloadQualityIndex => ref.read(audioSourcePresetsProvider - .select((s) => s.selectedDownloadingQualityIndex)); - - int get $downloadCount => dl - .getAllDownloads() - .where( - (download) => - download.status.value == DownloadStatus.downloading || - download.status.value == DownloadStatus.paused || - download.status.value == DownloadStatus.queued, - ) - .length; - - final Set $history; - // these are the tracks which metadata hasn't been fetched yet - final DownloadManager dl; - - String getTrackFileUrl(SourcedTrack track) { - final name = - "${track.query.name} - ${track.query.artists.map((e) => e.name).join(", ")}.${downloadContainer.getFileExtension()}"; - return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); - } - - bool isActive(SpotubeFullTrackObject track) { - if ($history.any((e) => e.query.id == track.id)) return true; - - final sourcedTrack = $history.firstWhereOrNull( - (element) => element.query.id == track.id, + DownloadTask copyWith({ + SpotubeFullTrackObject? track, + DownloadStatus? status, + CancelToken? cancelToken, + int? totalSizeBytes, + StreamController? downloadedBytesStreamController, + }) { + return DownloadTask( + track: track ?? this.track, + status: status ?? this.status, + cancelToken: cancelToken ?? this.cancelToken, + totalSizeBytes: totalSizeBytes ?? this.totalSizeBytes, + downloadedBytesStreamController: + downloadedBytesStreamController ?? _downloadedBytesStreamController, ); - - if (sourcedTrack == null) return false; - - return dl - .getAllDownloads() - .where( - (download) => - download.status.value == DownloadStatus.downloading || - download.status.value == DownloadStatus.paused || - download.status.value == DownloadStatus.queued, - ) - .map((e) => e.request.url) - .contains(sourcedTrack.getUrlOfQuality( - downloadContainer, - downloadQualityIndex, - )!); - } - - /// For singular downloads - Future addToQueue(SpotubeFullTrackObject track) async { - final sourcedTrack = await ref.read(sourcedTrackProvider(track).future); - - final savePath = getTrackFileUrl(sourcedTrack); - - final oldFile = File(savePath); - if (await oldFile.exists() && !await onFileExists(track)) { - return; - } - - if (await oldFile.exists()) { - await oldFile.rename("$savePath.old"); - } - - final downloadTask = await dl.addDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!, - savePath, - ); - if (downloadTask != null) { - $history.add(sourcedTrack); - } - notifyListeners(); - } - - Future batchAddToQueue(List tracks) async { - notifyListeners(); - for (final track in tracks) { - try { - if (track == tracks.first) { - await addToQueue(track); - } else { - await Future.delayed( - const Duration(seconds: 1), - () => addToQueue(track), - ); - } - } catch (e) { - AppLogger.reportError(e, StackTrace.current); - continue; - } - } - } - - Future removeFromQueue(SpotubeFullTrackObject track) async { - final sourcedTrack = await mapToSourcedTrack(track); - await dl.removeDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); - $history.remove(sourcedTrack); - } - - Future pause(SpotubeFullTrackObject track) async { - final sourcedTrack = await mapToSourcedTrack(track); - return dl.pauseDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); - } - - Future resume(SpotubeFullTrackObject track) async { - final sourcedTrack = await mapToSourcedTrack(track); - return dl.resumeDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); - } - - Future retry(SpotubeFullTrackObject track) { - return addToQueue(track); - } - - void cancel(SpotubeFullTrackObject track) async { - final sourcedTrack = await mapToSourcedTrack(track); - return dl.cancelDownload( - sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!); - } - - void cancelAll() { - for (final download in dl.getAllDownloads()) { - if (download.status.value == DownloadStatus.completed) continue; - dl.cancelDownload(download.request.url); - } - } - - Future mapToSourcedTrack(SpotubeFullTrackObject track) async { - final historicTrack = - $history.firstWhereOrNull((element) => element.query.id == track.id); - - if (historicTrack != null) { - return historicTrack; - } - - final sourcedTrack = await ref.read(sourcedTrackProvider(track).future); - - return sourcedTrack; - } - - ValueNotifier? getStatusNotifier( - SpotubeFullTrackObject track, - ) { - final sourcedTrack = $history.firstWhereOrNull( - (element) => element.query.id == track.id, - ); - if (sourcedTrack == null) { - return null; - } - return dl - .getDownload(sourcedTrack.getUrlOfQuality( - downloadContainer, downloadQualityIndex)!) - ?.status; - } - - ValueNotifier? getProgressNotifier(SpotubeFullTrackObject track) { - final sourcedTrack = $history.firstWhereOrNull( - (element) => element.query.id == track.id, - ); - if (sourcedTrack == null) { - return null; - } - return dl - .getDownload(sourcedTrack.getUrlOfQuality( - downloadContainer, downloadQualityIndex)!) - ?.progress; } } -final downloadManagerProvider = ChangeNotifierProvider( - (ref) => DownloadManagerProvider(ref: ref), +class DownloadManagerNotifier extends Notifier> { + final Dio dio; + DownloadManagerNotifier() + : dio = Dio(), + super(); + + @override + build() { + ref.onDispose(() { + for (final task in state) { + if (task.status == DownloadStatus.downloading) { + task.cancelToken.cancel(); + } + task._downloadedBytesStreamController.close(); + } + }); + + return []; + } + + DownloadTask? getTaskByTrackId(String trackId) { + return state.firstWhereOrNull((element) => element.track.id == trackId); + } + + void addToQueue(SpotubeFullTrackObject track) { + if (state.any((element) => element.track.id == track.id)) return; + state = [ + ...state, + DownloadTask( + track: track, + status: DownloadStatus.queued, + cancelToken: CancelToken(), + ), + ]; + + ref.read(sourcedTrackProvider(track)); + + _startDownloading(); // No await should be invoked to avoid stuck UI + } + + void addAllToQueue(List tracks) { + state = [ + ...state, + ...tracks.map((e) => DownloadTask( + track: e, + status: DownloadStatus.queued, + cancelToken: CancelToken(), + )), + ]; + + ref.read(sourcedTrackProvider(tracks.first)); + _startDownloading(); // No await should be invoked to avoid stuck UI + } + + void retry(SpotubeFullTrackObject track) { + if (state.firstWhereOrNull((e) => e.track.id == track.id)?.status + case DownloadStatus.canceled || DownloadStatus.failed) { + _setStatus(track, DownloadStatus.queued); + _startDownloading(); // No await should be invoked to avoid stuck UI + } + } + + void cancel(SpotubeFullTrackObject track) { + if (state.firstWhereOrNull((e) => e.track.id == track.id)?.status == + DownloadStatus.failed) { + return; + } + _setStatus(track, DownloadStatus.canceled); + } + + void clearAll() { + for (final task in state) { + if (task.status == DownloadStatus.downloading) { + task.cancelToken.cancel(); + } + } + state = []; + } + + void _setStatus(SpotubeFullTrackObject track, DownloadStatus status) { + state = state.map((e) { + if (e.track.id == track.id) { + if ((status == DownloadStatus.canceled) && e.cancelToken.isCancelled) { + e.cancelToken.cancel(); + } + + return e.copyWith(status: status); + } + return e; + }).toList(); + } + + bool _isShowingDialog = false; + + Future _shouldReplaceFileOnExist(DownloadTask task) async { + if (rootNavigatorKey.currentContext == null || _isShowingDialog) { + return false; + } + final replaceAll = ref.read(replaceDownloadedFileState); + if (replaceAll != null) return replaceAll; + _isShowingDialog = true; + try { + return await showDialog( + context: rootNavigatorKey.currentContext!, + builder: (context) => ReplaceDownloadedDialog( + track: task.track, + ), + ) ?? + false; + } finally { + _isShowingDialog = false; + } + } + + Future _downloadTrack(DownloadTask task) async { + try { + _setStatus(task.track, DownloadStatus.downloading); + final track = await ref.read(sourcedTrackProvider(task.track).future); + if (task.cancelToken.isCancelled) { + _setStatus(task.track, DownloadStatus.canceled); + } + final presets = ref.read(audioSourcePresetsProvider); + final container = + presets.presets[presets.selectedDownloadingContainerIndex]; + final downloadLocation = ref.read( + userPreferencesProvider.select((value) => value.downloadLocation)); + + final url = track.getUrlOfQuality( + container, + presets.selectedDownloadingQualityIndex, + ); + + if (url == null) { + throw Exception("No download URL found for selected codec"); + } + + final savePath = join( + downloadLocation, + ServiceUtils.sanitizeFilename( + "${track.query.name} - ${track.query.artists.map((e) => e.name).join(", ")}.${container.getFileExtension()}", + ), + ); + + final savePathFile = File(savePath); + if (await savePathFile.exists()) { + // dio automatically replaces the file if it exists so no deletion required + if (!await _shouldReplaceFileOnExist(task)) { + _setStatus(track.query, DownloadStatus.completed); + return; + } + } + + final response = await dio.chunkDownload( + url, + savePath, + cancelToken: task.cancelToken, + onReceiveProgress: (count, total) { + if (task.totalSizeBytes == null) { + state = state.map((e) { + if (e.track.id == track.query.id) { + return e.copyWith(totalSizeBytes: total); + } + return e; + }).toList(); + } + task._downloadedBytesStreamController.add(count); + }, + deleteOnError: true, + fileAccessMode: FileAccessMode.write, + ); + if (response.statusCode != null && response.statusCode! < 400) { + _setStatus(track.query, DownloadStatus.completed); + } else { + _setStatus(track.query, DownloadStatus.failed); + return; + } + + if (container.getFileExtension() == "weba") return; + + final imageBytes = await ServiceUtils.downloadImage( + (task.track.album.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + index: 1, + ), + ); + await MetadataGod.writeMetadata( + file: savePath, + metadata: task.track.toMetadata( + fileLength: await savePathFile.length(), + imageBytes: imageBytes, + ), + ); + } catch (e, stack) { + if (e is! DioException || e.type != DioExceptionType.cancel) { + _setStatus(task.track, DownloadStatus.failed); + AppLogger.reportError(e, stack); + } + } + } + + Future _startDownloading() async { + for (final task in state) { + if (task.status == DownloadStatus.downloading) return; + + if (task.status == DownloadStatus.queued) { + try { + await _downloadTrack(task); + } finally { + // After completion, check for more queued tasks + // Ignore errors of the prior task to allow next task to complete + await _startDownloading(); + } + } + } + } +} + +final downloadManagerProvider = + NotifierProvider>( + DownloadManagerNotifier.new, ); diff --git a/lib/provider/track_options/track_options_provider.dart b/lib/provider/track_options/track_options_provider.dart index e6b05201..d31aba73 100644 --- a/lib/provider/track_options/track_options_provider.dart +++ b/lib/provider/track_options/track_options_provider.dart @@ -49,7 +49,7 @@ class TrackOptionsActions { ref.read(metadataPluginSavedTracksProvider.notifier); MetadataPluginSavedPlaylistsNotifier get favoritePlaylistsNotifier => ref.read(metadataPluginSavedPlaylistsProvider.notifier); - DownloadManagerProvider get downloadManager => + DownloadManagerNotifier get downloadManager => ref.read(downloadManagerProvider.notifier); BlackListNotifier get blacklist => ref.read(blacklistProvider.notifier); @@ -263,7 +263,7 @@ typedef TrackOptionFlags = ({ bool isActiveTrack, bool isAuthenticated, bool isLiked, - ValueNotifier? progressNotifier, + DownloadTask? downloadTask, }); final trackOptionActionsProvider = @@ -283,15 +283,16 @@ final trackOptionsStateProvider = final isBlacklisted = blacklist.contains(track); final isSavedTrack = ref.watch(metadataPluginIsSavedTrackProvider(track.id)); + final downloadTask = playlist.activeTrack?.id == null + ? null + : downloadManager.getTaskByTrackId(playlist.activeTrack!.id); final isInDownloadQueue = playlist.activeTrack == null || playlist.activeTrack! is SpotubeLocalTrackObject ? false - : downloadManager - .isActive(playlist.activeTrack! as SpotubeFullTrackObject); - - final progressNotifier = track is SpotubeLocalTrackObject - ? null - : downloadManager.getProgressNotifier(track as SpotubeFullTrackObject); + : const [ + DownloadStatus.queued, + DownloadStatus.downloading, + ].contains(downloadTask?.status); return ( isInQueue: playlist.containsTrack(track), @@ -300,6 +301,6 @@ final trackOptionsStateProvider = isActiveTrack: playlist.activeTrack?.id == track.id, isAuthenticated: authenticated.asData?.value ?? false, isLiked: isSavedTrack.asData?.value ?? false, - progressNotifier: progressNotifier, + downloadTask: downloadTask, ); }); diff --git a/lib/services/download_manager/chunked_download.dart b/lib/services/download_manager/chunked_download.dart deleted file mode 100644 index 80a3e78f..00000000 --- a/lib/services/download_manager/chunked_download.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dio/dio.dart'; - -/// Downloading by spiting as file in chunks -extension ChunkDownload on Dio { - Future chunkedDownload( - url, { - Map? queryParameters, - required String savePath, - ProgressCallback? onReceiveProgress, - CancelToken? cancelToken, - bool deleteOnError = true, - int chunkSize = 102400, // 100KB - int maxConcurrentChunk = 3, - String tempExtension = ".temp", - }) async { - int total = 0; - var progress = []; - - ProgressCallback createCallback(int chunkIndex) { - return (int received, _) { - progress[chunkIndex] = received; - if (onReceiveProgress != null && total != 0) { - onReceiveProgress(progress.reduce((a, b) => a + b), total); - } - }; - } - - // this is the last response - // status & headers will the last chunk's status & headers - final completer = Completer(); - - Future downloadChunk( - String url, { - required int start, - required int end, - required int chunkIndex, - }) async { - progress.add(0); - --end; - final res = await download( - url, - savePath + tempExtension + chunkIndex.toString(), - onReceiveProgress: createCallback(chunkIndex), - cancelToken: cancelToken, - queryParameters: queryParameters, - deleteOnError: deleteOnError, - options: Options( - responseType: ResponseType.bytes, - headers: {"range": "bytes=$start-$end"}, - ), - ); - - return res; - } - - Future mergeTempFiles(int chunk) async { - File headFile = File("$savePath${tempExtension}0"); - var raf = await headFile.open(mode: FileMode.writeOnlyAppend); - for (int i = 1; i < chunk; ++i) { - File chunkFile = File(savePath + tempExtension + i.toString()); - raf = await raf.writeFrom(await chunkFile.readAsBytes()); - await chunkFile.delete(); - } - await raf.close(); - - headFile = await headFile.rename(savePath); - } - - final firstResponse = await downloadChunk( - url, - start: 0, - end: chunkSize, - chunkIndex: 0, - ); - - final responses = [firstResponse]; - - if (firstResponse.statusCode == HttpStatus.partialContent) { - total = int.parse( - firstResponse.headers - .value(HttpHeaders.contentRangeHeader) - ?.split("/") - .lastOrNull ?? - '0', - ); - - final reserved = total - - int.parse( - firstResponse.headers.value(HttpHeaders.contentLengthHeader) ?? - // since its a partial content, the content length will be the chunk size - chunkSize.toString(), - ); - - int chunk = (reserved / chunkSize).ceil() + 1; - - if (chunk > 1) { - int currentChunkSize = chunkSize; - if (chunk > maxConcurrentChunk + 1) { - chunk = maxConcurrentChunk + 1; - currentChunkSize = (reserved / maxConcurrentChunk).ceil(); - } - - responses.addAll( - await Future.wait( - List.generate(maxConcurrentChunk, (i) { - int start = chunkSize + i * currentChunkSize; - return downloadChunk( - url, - start: start, - end: start + currentChunkSize, - chunkIndex: i + 1, - ); - }), - ), - ); - } - - await mergeTempFiles(chunk).then((_) { - final response = responses.last; - final isPartialStatus = - response.statusCode == HttpStatus.partialContent; - - completer.complete( - Response( - data: response.data, - headers: response.headers, - requestOptions: response.requestOptions, - statusCode: isPartialStatus ? HttpStatus.ok : response.statusCode, - statusMessage: isPartialStatus ? 'Ok' : response.statusMessage, - extra: response.extra, - isRedirect: response.isRedirect, - redirects: response.redirects, - ), - ); - }).catchError((e) { - completer.completeError(e); - }); - } - - return completer.future; - } -} diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart deleted file mode 100644 index d2072bd7..00000000 --- a/lib/services/download_manager/download_manager.dart +++ /dev/null @@ -1,416 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; -import 'package:collection/collection.dart'; - -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; - -import 'package:spotube/services/download_manager/chunked_download.dart'; -import 'package:spotube/services/download_manager/download_request.dart'; -import 'package:spotube/services/download_manager/download_status.dart'; -import 'package:spotube/services/download_manager/download_task.dart'; -import 'package:spotube/services/logger/logger.dart'; -import 'package:spotube/utils/primitive_utils.dart'; - -export './download_request.dart'; -export './download_status.dart'; -export './download_task.dart'; - -typedef DownloadStatusEvent = ({ - DownloadStatus status, - DownloadRequest request -}); - -class DownloadManager { - final Map _cache = {}; - final Queue _queue = Queue(); - var dio = Dio(); - static const partialExtension = ".partial"; - static const tempExtension = ".temp"; - - // var tasks = StreamController(); - - final _statusStreamController = - StreamController.broadcast(); - Stream get statusStream => - _statusStreamController.stream; - - int maxConcurrentTasks = 2; - int runningTasks = 0; - - static final DownloadManager _dm = DownloadManager._internal(); - - DownloadManager._internal(); - - factory DownloadManager({int? maxConcurrentTasks}) { - if (maxConcurrentTasks != null) { - _dm.maxConcurrentTasks = maxConcurrentTasks; - } - return _dm; - } - - void Function(int, int) createCallback(url, int partialFileLength) => - (int received, int total) { - getDownload(url)?.progress.value = - (received + partialFileLength) / (total + partialFileLength); - - if (total == -1) {} - }; - - Future download( - String url, - String savePath, - CancelToken cancelToken, { - forceDownload = false, - }) async { - late String partialFilePath; - late File partialFile; - try { - final task = getDownload(url); - - if (task == null || task.status.value == DownloadStatus.canceled) { - return; - } - setStatus(task, DownloadStatus.downloading); - - final file = File(savePath.toString()); - - await Directory(path.dirname(savePath)).create(recursive: true); - - final tmpDirPath = await Directory( - path.join( - (await getTemporaryDirectory()).path, - "spotube-downloads", - ), - ).create(recursive: true); - - partialFilePath = path.join( - tmpDirPath.path, - path.basename(savePath) + partialExtension, - ); - partialFile = File(partialFilePath); - - final fileExist = await file.exists(); - final partialFileExist = await partialFile.exists(); - - if (fileExist) { - setStatus(task, DownloadStatus.completed); - } else if (partialFileExist) { - final partialFileLength = await partialFile.length(); - - final response = await dio.download( - url, - partialFilePath + tempExtension, - onReceiveProgress: createCallback(url, partialFileLength), - options: Options( - headers: { - HttpHeaders.rangeHeader: 'bytes=$partialFileLength-', - HttpHeaders.connectionHeader: "close", - }, - ), - cancelToken: cancelToken, - deleteOnError: true, - ); - - if (response.statusCode == HttpStatus.partialContent) { - final ioSink = partialFile.openWrite(mode: FileMode.writeOnlyAppend); - final partialChunkFile = File(partialFilePath + tempExtension); - await ioSink.addStream(partialChunkFile.openRead()); - await partialChunkFile.delete(); - await ioSink.close(); - - await partialFile.copy(savePath); - await partialFile.delete(); - - setStatus(task, DownloadStatus.completed); - } - } else { - final response = await dio.chunkedDownload( - url, - savePath: partialFilePath, - onReceiveProgress: createCallback(url, 0), - cancelToken: cancelToken, - deleteOnError: true, - ); - - if (response.statusCode == HttpStatus.ok) { - await partialFile.copy(savePath); - await partialFile.delete(); - setStatus(task, DownloadStatus.completed); - } - } - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - - var task = getDownload(url)!; - if (task.status.value != DownloadStatus.canceled && - task.status.value != DownloadStatus.paused) { - setStatus(task, DownloadStatus.failed); - runningTasks--; - - if (_queue.isNotEmpty) { - _startExecution(); - } - rethrow; - } else if (task.status.value == DownloadStatus.paused) { - final ioSink = partialFile.openWrite(mode: FileMode.writeOnlyAppend); - final f = File(partialFilePath + tempExtension); - if (await f.exists()) { - await ioSink.addStream(f.openRead()); - } - await ioSink.close(); - } - } - - runningTasks--; - - if (_queue.isNotEmpty) { - _startExecution(); - } - } - - void disposeNotifiers(DownloadTask task) { - // task.status.dispose(); - // task.progress.dispose(); - } - - void setStatus(DownloadTask? task, DownloadStatus status) { - if (task != null) { - task.status.value = status; - - // tasks.add(task); - if (status.isCompleted) { - disposeNotifiers(task); - } - - _statusStreamController.add((status: status, request: task.request)); - } - } - - Future addDownload(String url, String savedPath) async { - if (url.isEmpty) throw Exception("Invalid Url. Url is empty: $url"); - return _addDownloadRequest(DownloadRequest(url, savedPath)); - } - - Future _addDownloadRequest( - DownloadRequest downloadRequest, - ) async { - if (_cache[downloadRequest.url] != null) { - if (!_cache[downloadRequest.url]!.status.value.isCompleted && - _cache[downloadRequest.url]!.request == downloadRequest) { - // Do nothing - return _cache[downloadRequest.url]!; - } else { - _queue.remove(_cache[downloadRequest.url]?.request); - } - } - - _queue.add(DownloadRequest(downloadRequest.url, downloadRequest.path)); - - final task = DownloadTask(_queue.last); - - _cache[downloadRequest.url] = task; - - _startExecution(); - - return task; - } - - Future pauseDownload(String url) async { - var task = getDownload(url)!; - setStatus(task, DownloadStatus.paused); - task.request.cancelToken.cancel(); - - _queue.remove(task.request); - } - - Future cancelDownload(String url) async { - var task = getDownload(url)!; - setStatus(task, DownloadStatus.canceled); - _queue.remove(task.request); - task.request.cancelToken.cancel(); - } - - Future resumeDownload(String url) async { - var task = getDownload(url)!; - setStatus(task, DownloadStatus.downloading); - task.request.cancelToken = CancelToken(); - _queue.add(task.request); - - _startExecution(); - } - - Future removeDownload(String url) async { - cancelDownload(url); - _cache.remove(url); - } - - // Do not immediately call getDownload After addDownload, rather use the returned DownloadTask from addDownload - DownloadTask? getDownload(String url) { - return _cache[url]; - } - - Future whenDownloadComplete(String url, - {Duration timeout = const Duration(hours: 2)}) async { - DownloadTask? task = getDownload(url); - - if (task != null) { - return task.whenDownloadComplete(timeout: timeout); - } else { - return Future.error("Not found"); - } - } - - List getAllDownloads() { - return _cache.values.toList(); - } - - // Batch Download Mechanism - Future addBatchDownloads(List urls, String savePath) async { - for (final url in urls) { - addDownload(url, savePath); - } - } - - List getBatchDownloads(List urls) { - return urls.map((e) => _cache[e]).toList(); - } - - Future pauseBatchDownloads(List urls) async { - for (var element in urls) { - pauseDownload(element); - } - } - - Future cancelBatchDownloads(List urls) async { - for (var element in urls) { - cancelDownload(element); - } - } - - Future resumeBatchDownloads(List urls) async { - for (var element in urls) { - resumeDownload(element); - } - } - - ValueNotifier getBatchDownloadProgress(List urls) { - ValueNotifier progress = ValueNotifier(0); - var total = urls.length; - - if (total == 0) { - return progress; - } - - if (total == 1) { - return getDownload(urls.first)?.progress ?? progress; - } - - var progressMap = {}; - - for (var url in urls) { - DownloadTask? task = getDownload(url); - - if (task != null) { - progressMap[url] = 0.0; - - if (task.status.value.isCompleted) { - progressMap[url] = 1.0; - progress.value = progressMap.values.sum / total; - } - - void progressListener() { - progressMap[url] = task.progress.value; - progress.value = progressMap.values.sum / total; - } - - task.progress.addListener(progressListener); - - void listener() { - if (task.status.value.isCompleted) { - progressMap[url] = 1.0; - progress.value = progressMap.values.sum / total; - task.status.removeListener(listener); - task.progress.removeListener(progressListener); - } - } - - task.status.addListener(listener); - } else { - total--; - } - } - - return progress; - } - - Future?> whenBatchDownloadsComplete(List urls, - {Duration timeout = const Duration(hours: 2)}) async { - var completer = Completer?>(); - - var completed = 0; - var total = urls.length; - - for (final url in urls) { - DownloadTask? task = getDownload(url); - - if (task != null) { - if (task.status.value.isCompleted) { - completed++; - - if (completed == total) { - completer.complete(getBatchDownloads(urls)); - } - } - - void listener() { - if (task.status.value.isCompleted) { - completed++; - - if (completed == total) { - completer.complete(getBatchDownloads(urls)); - task.status.removeListener(listener); - } - } - } - - task.status.addListener(listener); - } else { - total--; - - if (total == 0) { - completer.complete(null); - } - } - } - - return completer.future.timeout(timeout); - } - - void _startExecution() async { - if (runningTasks == maxConcurrentTasks || _queue.isEmpty) { - return; - } - - while (_queue.isNotEmpty && runningTasks < maxConcurrentTasks) { - runningTasks++; - var currentRequest = _queue.removeFirst(); - - await download( - currentRequest.url, - currentRequest.path, - currentRequest.cancelToken, - ); - - await Future.delayed(const Duration(milliseconds: 500), null); - } - } - - /// This function is used for get file name with extension from url - String getFileNameFromUrl(String url) { - return PrimitiveUtils.toSafeFileName(url.split('/').last); - } -} diff --git a/lib/services/download_manager/download_request.dart b/lib/services/download_manager/download_request.dart deleted file mode 100644 index 80c4af37..00000000 --- a/lib/services/download_manager/download_request.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:dio/dio.dart'; - -class DownloadRequest { - final String url; - final String path; - var cancelToken = CancelToken(); - var forceDownload = false; - - DownloadRequest( - this.url, - this.path, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is DownloadRequest && - runtimeType == other.runtimeType && - url == other.url && - path == other.path; - - @override - int get hashCode => url.hashCode ^ path.hashCode; -} diff --git a/lib/services/download_manager/download_status.dart b/lib/services/download_manager/download_status.dart deleted file mode 100644 index b97080fa..00000000 --- a/lib/services/download_manager/download_status.dart +++ /dev/null @@ -1,26 +0,0 @@ -enum DownloadStatus { - queued, - downloading, - completed, - failed, - paused, - canceled; - - bool get isCompleted { - switch (this) { - case DownloadStatus.queued: - return false; - case DownloadStatus.downloading: - return false; - case DownloadStatus.paused: - return false; - case DownloadStatus.completed: - return true; - case DownloadStatus.failed: - return true; - - case DownloadStatus.canceled: - return true; - } - } -} diff --git a/lib/services/download_manager/download_task.dart b/lib/services/download_manager/download_task.dart deleted file mode 100644 index d79cf95b..00000000 --- a/lib/services/download_manager/download_task.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:spotube/services/download_manager/download_request.dart'; -import 'package:spotube/services/download_manager/download_status.dart'; - -class DownloadTask { - final DownloadRequest request; - ValueNotifier status = ValueNotifier(DownloadStatus.queued); - ValueNotifier progress = ValueNotifier(0); - - DownloadTask( - this.request, - ); - - Future whenDownloadComplete( - {Duration timeout = const Duration(hours: 2)}) async { - var completer = Completer(); - - if (status.value.isCompleted) { - completer.complete(status.value); - } - - void listener() { - if (status.value.isCompleted) { - completer.complete(status.value); - status.removeListener(listener); - } - } - - status.addListener(listener); - - return completer.future.timeout(timeout); - } -} From cab09e00cee54ec8ee05b7715d4f8dee8ba91fde Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Nov 2025 10:02:37 +0600 Subject: [PATCH 33/47] chore: ignore DB queries in migrations --- lib/models/database/database.dart | 52 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 8df70968..f1c66c1a 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,6 +19,7 @@ import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -200,26 +201,41 @@ class AppDatabase extends _$AppDatabase { }); }, from8To9: (m, schema) async { - await m.renameTable(schema.pluginsTable, "metadata_plugins_table"); - await m.renameColumn( - schema.pluginsTable, - "selected", - pluginsTable.selectedForMetadata, - ); - await m.addColumn( - schema.pluginsTable, - pluginsTable.selectedForAudioSource, - ); + await m + .renameTable(schema.pluginsTable, "metadata_plugins_table") + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await m + .renameColumn( + schema.pluginsTable, + "selected", + pluginsTable.selectedForMetadata, + ) + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await m + .addColumn( + schema.pluginsTable, + pluginsTable.selectedForAudioSource, + ) + .catchError((e, stack) => AppLogger.reportError(e, stack)); }, from9To10: (m, schema) async { - await m.dropColumn(schema.preferencesTable, "piped_instance"); - await m.dropColumn(schema.preferencesTable, "invidious_instance"); - await m.addColumn( - schema.sourceMatchTable, - sourceMatchTable.sourceInfo, - ); - await customStatement("DROP INDEX IF EXISTS uniq_track_match;"); - await m.dropColumn(schema.sourceMatchTable, "source_id"); + await m + .dropColumn(schema.preferencesTable, "piped_instance") + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await m + .dropColumn(schema.preferencesTable, "invidious_instance") + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await m + .addColumn( + schema.sourceMatchTable, + sourceMatchTable.sourceInfo, + ) + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await customStatement("DROP INDEX IF EXISTS uniq_track_match;") + .catchError((e, stack) => AppLogger.reportError(e, stack)); + await m + .dropColumn(schema.sourceMatchTable, "source_id") + .catchError((e, stack) => AppLogger.reportError(e, stack)); }, ), ); From bb6f4bd57b3998d240e03f5f9f1a8fb2ad2b1c0a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Nov 2025 11:17:34 +0600 Subject: [PATCH 34/47] feat(android): add 16KB page size support --- android/app/build.gradle | 2 +- pubspec.lock | 98 ++++++++++++++++++---------------------- pubspec.yaml | 38 ++-------------- 3 files changed, 49 insertions(+), 89 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d8e35b29..7319c6a8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -36,7 +36,7 @@ android { compileSdkVersion 36 - ndkVersion = "27.0.12077973" + ndkVersion = "29.0.14206865" compileOptions { coreLibraryDesugaringEnabled true diff --git a/pubspec.lock b/pubspec.lock index 33275111..f2ed4dd1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -992,14 +992,13 @@ packages: source: hosted version: "9.2.4" flutter_secure_storage_linux: - dependency: "direct overridden" + dependency: transitive description: - path: flutter_secure_storage_linux - ref: patch-2 - resolved-ref: f076cbb65b075afd6e3b648122987a67306dc298 - url: "https://github.com/m-berto/flutter_secure_storage.git" - source: git - version: "2.0.1" + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" flutter_secure_storage_macos: dependency: transitive description: @@ -1009,13 +1008,13 @@ packages: source: hosted version: "3.1.3" flutter_secure_storage_platform_interface: - dependency: "direct overridden" + dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: b8337d3d52e429e6c0a7710e38cf9742a3bb05844bd927450eb94f80c11ef85d + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: @@ -1530,65 +1529,58 @@ packages: media_kit: dependency: "direct main" description: - path: media_kit - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git - version: "1.2.0" + name: media_kit + sha256: "52a8e989babc431db0aa242f32a4a08e55f60662477ea09759a105d7cd6410da" + url: "https://pub.dev" + source: hosted + version: "1.2.1" media_kit_libs_android_audio: - dependency: "direct overridden" + dependency: transitive description: - path: "libs/android/media_kit_libs_android_audio" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git - version: "1.3.7" + name: media_kit_libs_android_audio + sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b" + url: "https://pub.dev" + source: hosted + version: "1.3.8" media_kit_libs_audio: dependency: "direct main" description: - path: "libs/universal/media_kit_libs_audio" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git - version: "1.0.6" + name: media_kit_libs_audio + sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316" + url: "https://pub.dev" + source: hosted + version: "1.0.7" media_kit_libs_ios_audio: - dependency: "direct overridden" + dependency: transitive description: - path: "libs/ios/media_kit_libs_ios_audio" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git + name: media_kit_libs_ios_audio + sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f" + url: "https://pub.dev" + source: hosted version: "1.1.4" media_kit_libs_linux: - dependency: "direct overridden" + dependency: transitive description: - path: "libs/linux/media_kit_libs_linux" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git + name: media_kit_libs_linux + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" + url: "https://pub.dev" + source: hosted version: "1.2.1" media_kit_libs_macos_audio: - dependency: "direct overridden" + dependency: transitive description: - path: "libs/macos/media_kit_libs_macos_audio" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git + name: media_kit_libs_macos_audio + sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950" + url: "https://pub.dev" + source: hosted version: "1.1.4" media_kit_libs_windows_audio: - dependency: "direct overridden" + dependency: transitive description: - path: "libs/windows/media_kit_libs_windows_audio" - ref: HEAD - resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 - url: "https://github.com/media-kit/media-kit" - source: git + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted version: "1.0.9" menu_base: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 65cf4576..6ace34d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,7 +61,6 @@ dependencies: sdk: flutter flutter_native_splash: ^2.4.6 flutter_riverpod: ^2.5.1 - flutter_secure_storage: ^9.2.4 flutter_sharing_intent: ^1.1.0 flutter_undraw: ^0.2.1 form_builder_validators: ^11.1.1 @@ -82,14 +81,8 @@ dependencies: logger: ^2.0.2 logging: ^1.3.0 lrc: ^1.0.2 - media_kit: - git: - url: https://github.com/media-kit/media-kit - path: media_kit - media_kit_libs_audio: - git: - url: https://github.com/media-kit/media-kit - path: libs/universal/media_kit_libs_audio + media_kit: ^1.2.1 + media_kit_libs_audio: ^1.0.7 metadata_god: ^1.1.0 mime: ^2.0.0 open_file: ^3.5.10 @@ -161,6 +154,7 @@ dependencies: flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 change_case: ^1.1.0 + flutter_secure_storage: ^9.2.4 dev_dependencies: build_runner: ^2.4.13 @@ -191,32 +185,6 @@ dependency_overrides: flutter_svg: ^2.0.17 intl: any collection: any - flutter_secure_storage_linux: - git: - url: https://github.com/m-berto/flutter_secure_storage.git - ref: patch-2 - path: flutter_secure_storage_linux - flutter_secure_storage_platform_interface: 2.0.0 - media_kit_libs_android_audio: - git: - url: https://github.com/media-kit/media-kit - path: libs/android/media_kit_libs_android_audio - media_kit_libs_ios_audio: - git: - url: https://github.com/media-kit/media-kit - path: libs/ios/media_kit_libs_ios_audio - media_kit_libs_macos_audio: - git: - url: https://github.com/media-kit/media-kit - path: libs/macos/media_kit_libs_macos_audio - media_kit_libs_windows_audio: - git: - url: https://github.com/media-kit/media-kit - path: libs/windows/media_kit_libs_windows_audio - media_kit_libs_linux: - git: - url: https://github.com/media-kit/media-kit - path: libs/linux/media_kit_libs_linux flutter: generate: true From 6884a131c92c2e7fed1bf5ec8427049685048e83 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Nov 2025 14:35:06 +0600 Subject: [PATCH 35/47] fix(playback): use stream instead of chunked serving of audio bytes --- .../user_local_tracks/local_folder.dart | 4 +- lib/provider/audio_player/state.dart | 14 +- lib/provider/server/routes/playback.dart | 184 ++++++++---------- pubspec.lock | 19 +- pubspec.yaml | 6 + 5 files changed, 113 insertions(+), 114 deletions(-) diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index 55a148f6..523097e1 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -198,7 +198,7 @@ class LocalLibraryPage extends HookConsumerWidget { ), ); - if (accepted ?? false) return; + if (accepted != true) return; final cacheDir = Directory( await UserPreferencesNotifier.getMusicCacheDir(), @@ -207,6 +207,8 @@ class LocalLibraryPage extends HookConsumerWidget { if (cacheDir.existsSync()) { await cacheDir.delete(recursive: true); } + + ref.invalidate(localTracksProvider); }, ), IconButton.outline( diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 7bd47f76..d62155f3 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -51,15 +51,17 @@ class AudioPlayerState with _$AudioPlayerState { } bool containsTrack(SpotubeTrackObject track) { - return tracks.any( - (t) => t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject - ? t.path == track.path - : t.id == track.id, - ); + return tracks.isNotEmpty && + tracks.any( + (t) => + t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject + ? t.path == track.path + : t.id == track.id, + ); } bool containsTracks(List tracks) { - return tracks.every(containsTrack); + return this.tracks.isNotEmpty && tracks.every(containsTrack); } bool containsCollection(String collectionId) { diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index f7085505..ef64481c 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; @@ -124,8 +125,7 @@ class ServerPlaybackRoutes { return res; } - Future<({dio_lib.Response response, Uint8List? bytes})> - streamTrack( + Future streamTrack( Request request, SourcedTrack track, Map headers, @@ -141,30 +141,29 @@ class ServerPlaybackRoutes { final bytes = await trackCacheFile.readAsBytes(); final cachedFileLength = bytes.length; - return ( - response: dio_lib.Response( - statusCode: 200, - headers: Headers.fromMap({ - "content-type": ["audio/${track.qualityPreset!.name}"], - "content-length": ["$cachedFileLength"], - "accept-ranges": ["bytes"], - "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], - }), - requestOptions: RequestOptions(path: request.requestedUri.toString()), - ), - bytes: bytes, + return dio_lib.Response( + statusCode: 200, + headers: Headers.fromMap({ + "content-type": ["audio/${track.qualityPreset!.name}"], + "content-length": ["${cachedFileLength - 1}"], + "accept-ranges": ["bytes"], + "content-range": [ + "bytes 0-${cachedFileLength - 1}/$cachedFileLength" + ], + "connection": ["close"], + }), + requestOptions: RequestOptions(path: request.requestedUri.toString()), + data: bytes, ); } - final trackPartialCacheFile = File("${trackCacheFile.path}.part"); - String url = track.url ?? await ref .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); - var options = Options( + final options = Options( headers: { ...headers, "user-agent": _randomUserAgent, @@ -172,12 +171,15 @@ class ServerPlaybackRoutes { "Connection": "keep-alive", "host": Uri.parse(url).host, }, - responseType: ResponseType.bytes, + responseType: ResponseType.stream, validateStatus: (status) => status! < 400, ); final contentLengthRes = await Future.value( - dio.head(url, options: options), + dio.head( + url, + options: options.copyWith(responseType: ResponseType.bytes), + ), ).catchError((e, stack) async { AppLogger.reportError(e, stack); @@ -193,39 +195,19 @@ class ServerPlaybackRoutes { // Redirect to m3u8 link directly as it handles range requests internally if (contentLengthRes?.headers.value("content-type") == "application/vnd.apple.mpegurl") { - return ( - response: dio_lib.Response( - statusCode: 301, - statusMessage: "M3U8 Redirect", - headers: Headers.fromMap({ - "location": [url], - "content-type": ["application/vnd.apple.mpegurl"], - }), - requestOptions: RequestOptions(path: request.requestedUri.toString()), - isRedirect: true, - ), - bytes: null, + return dio_lib.Response( + statusCode: 301, + statusMessage: "M3U8 Redirect", + headers: Headers.fromMap({ + "location": [url], + "content-type": ["application/vnd.apple.mpegurl"], + }), + requestOptions: RequestOptions(path: request.requestedUri.toString()), + isRedirect: true, ); } - if (headers["range"] == "bytes=0-" && - track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) { - const bufferSize = 6 * 1024 * 1024; // 6MB for lossless - - final endRange = min( - bufferSize, - int.parse(contentLengthRes?.headers.value("content-length") ?? "0"), - ); - - options = options.copyWith( - headers: { - ...?options.headers, - "range": "bytes=0-$endRange", - }, - ); - } - - final res = await dio.get(url, options: options); + final res = await dio.get(url, options: options); AppLogger.log.i( "Response for track: ${track.query.name}\n" @@ -233,66 +215,64 @@ class ServerPlaybackRoutes { "Headers: ${res.headers.map}", ); - final bytes = res.data; - - if (bytes == null || !userPreferences.cacheMusic) { - return (response: res, bytes: bytes); + if (!userPreferences.cacheMusic) { + return res; } - final contentRange = - ContentRangeHeader.parse(res.headers.value("content-range") ?? ""); + final resStream = res.data!.stream.asBroadcastStream(); + final trackPartialCacheFile = File("${trackCacheFile.path}.part"); if (!await trackPartialCacheFile.exists()) { await trackPartialCacheFile.create(recursive: true); } // Write the stream to the file based on the range - final partialCacheFile = - await trackPartialCacheFile.open(mode: FileMode.writeOnlyAppend); - int fileLength = 0; - try { - await partialCacheFile.setPosition(contentRange.start); - await partialCacheFile.writeFrom(bytes); - fileLength = await partialCacheFile.length(); - } finally { - await partialCacheFile.close(); - } + final partialCacheFileSink = + trackPartialCacheFile.openWrite(mode: FileMode.writeOnlyAppend); + final contentRange = res.headers.value("content-range") != null + ? ContentRangeHeader.parse(res.headers.value("content-range") ?? "") + : ContentRangeHeader(0, 0, 0); - if (fileLength == contentRange.total) { - await trackPartialCacheFile.rename(trackCacheFile.path); - } + resStream.listen( + (data) { + partialCacheFileSink.add(data); + }, + onError: (e, stack) { + partialCacheFileSink.close(); + }, + onDone: () async { + await partialCacheFileSink.close(); - if (contentRange.total == fileLength && - track.qualityPreset!.getFileExtension() != "weba") { - final playlistTrack = playlist.tracks.firstWhereOrNull( - (element) => element.id == track.query.id, - ); - if (playlistTrack == null) { - AppLogger.log.e( - "Track ${track.query.id} not found in playlist, cannot write metadata.", + final fileLength = await trackPartialCacheFile.length(); + if (fileLength != contentRange.total) return; + + await trackPartialCacheFile.rename(trackCacheFile.path); + + if (track.qualityPreset!.getFileExtension() == "weba") return; + + final imageBytes = await ServiceUtils.downloadImage( + track.query.album.images.asUrlString( + placeholder: ImagePlaceholder.albumArt, + index: 1, + ), ); - return (response: res, bytes: bytes); - } - final imageBytes = await ServiceUtils.downloadImage( - (playlistTrack.album.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - index: 1, - ), - ); + await MetadataGod.writeMetadata( + file: trackCacheFile.path, + metadata: track.query.toMetadata( + imageBytes: imageBytes, + fileLength: fileLength, + ), + ).catchError((e, stackTrace) { + AppLogger.reportError(e, stackTrace); + }); + }, + cancelOnError: true, + ); - await MetadataGod.writeMetadata( - file: trackCacheFile.path, - metadata: (playlistTrack as SpotubeFullTrackObject).toMetadata( - imageBytes: imageBytes, - fileLength: fileLength, - ), - ).catchError((e, stackTrace) { - AppLogger.reportError(e, stackTrace); - }); - } - - return (bytes: bytes, response: res); + res.data?.stream = + resStream; // To avoid Stream has been already listened to exception + return res; } /// @head('/stream/') @@ -328,15 +308,23 @@ class ServerPlaybackRoutes { return Response.notFound("Track not found in the current queue"); } - final (bytes: audioBytes, response: res) = await streamTrack( + final res = await streamTrack( request, sourcedTrack, request.headers, ); + if (res.data is ResponseBody) { + return Response( + res.statusCode!, + body: (res.data as ResponseBody).stream, + headers: res.headers.map, + ); + } + return Response( res.statusCode!, - body: audioBytes, + body: res.data, headers: res.headers.map, ); } catch (e, stack) { diff --git a/pubspec.lock b/pubspec.lock index f2ed4dd1..85493ee5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -992,13 +992,14 @@ packages: source: hosted version: "9.2.4" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" - source: hosted - version: "1.2.3" + path: flutter_secure_storage_linux + ref: patch-2 + resolved-ref: f076cbb65b075afd6e3b648122987a67306dc298 + url: "https://github.com/m-berto/flutter_secure_storage.git" + source: git + version: "2.0.1" flutter_secure_storage_macos: dependency: transitive description: @@ -1008,13 +1009,13 @@ packages: source: hosted version: "3.1.3" flutter_secure_storage_platform_interface: - dependency: transitive + dependency: "direct overridden" description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: b8337d3d52e429e6c0a7710e38cf9742a3bb05844bd927450eb94f80c11ef85d url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.0" flutter_secure_storage_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6ace34d0..d48f65d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -185,6 +185,12 @@ dependency_overrides: flutter_svg: ^2.0.17 intl: any collection: any + flutter_secure_storage_platform_interface: 2.0.0 + flutter_secure_storage_linux: + git: + url: https://github.com/m-berto/flutter_secure_storage.git + ref: patch-2 + path: flutter_secure_storage_linux flutter: generate: true From d843ce9ede9559ff8fe6c96200fe98e547afd455 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Nov 2025 15:55:07 +0600 Subject: [PATCH 36/47] chore: upgrade yt plugin --- .../plugin.smplug | Bin 19827 -> 20876 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/plugins/spotube-plugin-youtube-audio/plugin.smplug b/assets/plugins/spotube-plugin-youtube-audio/plugin.smplug index 4b93b31b624d3f971de5e49b249a15e9fe0dfaea..f8dedd0c268be48959b60ec91d95d73854e9a4d0 100644 GIT binary patch delta 3523 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 delta 2463 zcmV;Q31Ie&qXF}p0S!<~0|XQR000O8RkCN14Iu~N8xCoXrU{WX@PAdZXIp)mu79Zr z008wA000UA8~|`^b!TaAE^l>oRa6N81K}GEX^y4|X?1uD009K(0{{R7=mP)%y;$o~ z9LE*zS(e)(A;fKv9g#M|l89gz5W)yuSQ&#Q8zdsokCaM9_3k!oi@B`k0;XJkggir@ zBvtvDw@4~gsl0_N-+$?zncZ1X*^#RvMm5vt*5{l)x88x?@d;LB1*ZRvEiW!VSXf$G zSp14Ft*$Svt*OfUcf!-hTv&I3oYMP*dS6QE%)S`D0(ZW6Nu_eB}|ia2{#p>b@67L97j%nmv{g z0|py^bG-=LGPCE4AsZ6;ky>EF2Xn?XLcsieLnxi)XUt z638XLYHTlX^^=^tD4Z-ot*DM94cir-I+9_O;~|R&+{-Wz zv{ssvoD&Chbc67GBoKD`fXRc74E1Jpzah!29wF%U&2P$vx*sH2P!?um%$iuD?Y{VKQVL8Ngn=qq!;3&by@~|<2jay_5 zPfsU_2s)k08Rg~1>66(X7k{!YC}d~$O{DgrdreJF*(}3M<}vedt*zb9a1>J(EwoqR zz#il88;qcZ)G^6xijA<(jbD4LOk$xQ>whV>JUIo%&n)&iu@+hTpE)9n-9+pAnb-%x zW_*n8eg`{lLW60%e~&i@x|pD`^A{GjfQNDbs~b@02K0Q)Gxp^>&-@>DhU;&WFaNo% zyaw0h7S)66eC4K>hy2J7xZua?RC-+Zi!y%^@w)JMAW@V&?t7Ah#*LjwA%S_)Uw_J_ zgQnz1DhMM3;wt3S_$YN{gQHFGnoN=9Qne)l;hHV@0!M#;oD2d71Turb(^A#!8gf$- zA5+6k2|KD!$MEp{m^gM7rkn8J%X zg}+Qi-_BBajxDn5puFAYpSC-5;eSplVgRf~cGqS$HlO?0=6|pYC5x>U23Oh53z$^q zJ%oupL0QnQHuLE|+qy8!!Q!!Q0Y!F4r~M$MHlO`*>8C=TWZJd?7@?rc1TQ3lt)lip zIadv}4;I*U_U{YjuL3?;(WjNMhuL37;l;~Uz6Xh)tQUKx^;){9*AlA441Xw#?Jav# zra|qcKFek}tf@(={J%&zsYy*v{c9UJK{0MJM+f}ZcljTaD#wJZ zTDIuv%BuU9WYr^N->eCMj(_7_NnJRuU3fp=*DjvpKlC|1NH0zU~l^5FfZkQRRnl)0yeYM?4H`N79f-zK-Q&3M{?N4XE#^vcHc}$`ky1T!-#JfO4b`Qc#vlr58seBapzL zc^Jv&^@xP>rZFD>x54gbt@e9^!}_>M0;@J$aVu z5xVrD^(d1RTrxxC&VM^fo-R^C*PFsSmW~gD%w&r+J)egn8AAJnI1}yfx*|9sC3tpG zn=_Vreo6!1eB4MccARpFeQnWG%_q`kK9Rm5_!w$dFCsQ6GV*njx1>{|0#_cibWN2= z^)*%kg>sN+3=F1fKq6dc%cXt89dhN0V;Sm_;wbL~Wr=NB)PFRp-80m;q|puhl_-ab z?0HiA3V+>CE*@rYGp;vyas^2bD!HA|#1YEiRMlnMi@bK83%Hp!@eC#ngjx;MGU7r9 zlc*`GePmw;7lmQzZL+HlQWH~2-K32>^pKs7n};f>o+8xIry4rwXr4(bG3@aob)-;x zlUOpN5mDD`6A z@A5(ps+|t1e)NPcsvT4Ace8T;(xOFL?&%5z;Hw<4YJht=U`J!vy>P?2hCgXq>qc_J zx}m%EPO_qV=4&MXAE{;2#cK~U0o3ifj=j{-s?|9&?O6%SE4r&0Kkkzs{|it{0|XQR z000O8RRXeSlP(S(2jLqIX^y4|vr`U0LIYK@XOostF9YEl4wJ}G91c~oXIp)mu79Zr z008wAlk!j_1K}GElgLmUGF7r?Th3u{C?rAv0KrKB00;mW00000000000HlEk3IG6X dZ)a~VaBgQ+R0RM7;TsNFvpZcG3G001fNjwt{D From 11866d532b83ac861b51f5d0071f2fb8716a188b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Nov 2025 14:57:08 +0600 Subject: [PATCH 37/47] chore: remove useless dependencies --- lib/l10n/app_en.arb | 6 +- lib/l10n/generated/app_localizations.dart | 24 +- lib/l10n/generated/app_localizations_ar.dart | 10 +- lib/l10n/generated/app_localizations_bn.dart | 10 +- lib/l10n/generated/app_localizations_ca.dart | 10 +- lib/l10n/generated/app_localizations_cs.dart | 10 +- lib/l10n/generated/app_localizations_de.dart | 10 +- lib/l10n/generated/app_localizations_en.dart | 10 +- lib/l10n/generated/app_localizations_es.dart | 10 +- lib/l10n/generated/app_localizations_eu.dart | 10 +- lib/l10n/generated/app_localizations_fa.dart | 10 +- lib/l10n/generated/app_localizations_fi.dart | 10 +- lib/l10n/generated/app_localizations_fr.dart | 10 +- lib/l10n/generated/app_localizations_hi.dart | 10 +- lib/l10n/generated/app_localizations_id.dart | 10 +- lib/l10n/generated/app_localizations_it.dart | 10 +- lib/l10n/generated/app_localizations_ja.dart | 10 +- lib/l10n/generated/app_localizations_ka.dart | 10 +- lib/l10n/generated/app_localizations_ko.dart | 10 +- lib/l10n/generated/app_localizations_ne.dart | 10 +- lib/l10n/generated/app_localizations_nl.dart | 10 +- lib/l10n/generated/app_localizations_pl.dart | 10 +- lib/l10n/generated/app_localizations_pt.dart | 10 +- lib/l10n/generated/app_localizations_ru.dart | 10 +- lib/l10n/generated/app_localizations_ta.dart | 10 +- lib/l10n/generated/app_localizations_th.dart | 10 +- lib/l10n/generated/app_localizations_tl.dart | 10 +- lib/l10n/generated/app_localizations_tr.dart | 10 +- lib/l10n/generated/app_localizations_uk.dart | 10 +- lib/l10n/generated/app_localizations_vi.dart | 10 +- lib/l10n/generated/app_localizations_zh.dart | 16 +- lib/modules/player/sibling_tracks_sheet.dart | 219 +++++++++--------- lib/pages/settings/sections/playback.dart | 16 +- lib/provider/history/summary.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 145 ++++-------- pubspec.yaml | 42 +++- untranslated_messages.json | 116 ++++++++++ .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 42 files changed, 566 insertions(+), 311 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a292105d..111d76a8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -264,8 +264,10 @@ "change_cover": "Change cover", "add_cover": "Add cover", "restore_defaults": "Restore defaults", - "download_music_codec": "Download music codec", - "streaming_music_codec": "Streaming music codec", + "download_music_format": "Download music format", + "streaming_music_format": "Streaming music format", + "download_music_quality": "Download music quality", + "streaming_music_quality": "Streaming music quality", "login_with_lastfm": "Login with Last.fm", "connect": "Connect", "disconnect_lastfm": "Disconnect Last.fm", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 0ffcffff..e9d7913d 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1743,17 +1743,29 @@ abstract class AppLocalizations { /// **'Restore defaults'** String get restore_defaults; - /// No description provided for @download_music_codec. + /// No description provided for @download_music_format. /// /// In en, this message translates to: - /// **'Download music codec'** - String get download_music_codec; + /// **'Download music format'** + String get download_music_format; - /// No description provided for @streaming_music_codec. + /// No description provided for @streaming_music_format. /// /// In en, this message translates to: - /// **'Streaming music codec'** - String get streaming_music_codec; + /// **'Streaming music format'** + String get streaming_music_format; + + /// No description provided for @download_music_quality. + /// + /// In en, this message translates to: + /// **'Download music quality'** + String get download_music_quality; + + /// No description provided for @streaming_music_quality. + /// + /// In en, this message translates to: + /// **'Streaming music quality'** + String get streaming_music_quality; /// No description provided for @login_with_lastfm. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 05da9c97..5f17edb0 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -874,10 +874,16 @@ class AppLocalizationsAr extends AppLocalizations { String get restore_defaults => 'استعادة الإعدادات الافتراضية'; @override - String get download_music_codec => 'تنزيل ترميز الموسيقى'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'ترميز الموسيقى بالتدفق'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'تسجيل الدخول باستخدام Last.fm'; diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index b11a9f2f..7967d63f 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -873,10 +873,16 @@ class AppLocalizationsBn extends AppLocalizations { String get restore_defaults => 'ডিফল্ট সেটিংস পুনরুদ্ধার করুন'; @override - String get download_music_codec => 'সঙ্গীত কোডেক ডাউনলোড করুন'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'স্ট্রিমিং সঙ্গীত কোডেক'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm দিয়ে লগইন করুন'; diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index a18d8c38..5b6a3138 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -876,10 +876,16 @@ class AppLocalizationsCa extends AppLocalizations { String get restore_defaults => 'Restaura els valors per defecte'; @override - String get download_music_codec => 'Descarrega el codec de música'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Codec de música en streaming'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Inicia la sessió amb Last.fm'; diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index ce5785d4..0f28b4ef 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -869,10 +869,16 @@ class AppLocalizationsCs extends AppLocalizations { String get restore_defaults => 'Obnovit výchozí'; @override - String get download_music_codec => 'Kodek pro stahování'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Kodek pro streamování'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Přihlásit se pomocí Last.fm'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 81a67861..b5b21b86 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -879,10 +879,16 @@ class AppLocalizationsDe extends AppLocalizations { String get restore_defaults => 'Standardeinstellungen wiederherstellen'; @override - String get download_music_codec => 'Musik-Codec herunterladen'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Streaming-Musik-Codec'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Mit Last.fm anmelden'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 513daa77..83a2c24c 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -871,10 +871,16 @@ class AppLocalizationsEn extends AppLocalizations { String get restore_defaults => 'Restore defaults'; @override - String get download_music_codec => 'Download music codec'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Streaming music codec'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Login with Last.fm'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 08426481..ed595693 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -876,10 +876,16 @@ class AppLocalizationsEs extends AppLocalizations { String get restore_defaults => 'Restaurar valores predeterminados'; @override - String get download_music_codec => 'Descargar códec de música'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Códec de música en streaming'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Iniciar sesión con Last.fm'; diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 14b8e01f..dbc52fdb 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -876,10 +876,16 @@ class AppLocalizationsEu extends AppLocalizations { String get restore_defaults => 'Berrezarri berezko balioak'; @override - String get download_music_codec => 'Deskargatutako musikaren codec-a'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Streaming musikaren codec-a'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Hasi saioa Last.fm-n'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index d0f73246..ecd3a5c1 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -870,10 +870,16 @@ class AppLocalizationsFa extends AppLocalizations { String get restore_defaults => 'بازیابی پیش فرض ها'; @override - String get download_music_codec => 'دانلود کدک موسیقی'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'کدک موسیقی استریمینگ'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'ورود با Last.fm'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 751eb0c1..bec4cbea 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -872,10 +872,16 @@ class AppLocalizationsFi extends AppLocalizations { String get restore_defaults => 'Palauta oletukset'; @override - String get download_music_codec => 'Ladatun musiikin codefc'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Suoratoistetun musiikin codec'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Kirjaudu sisään Last.fm:llä'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 068701cc..e6fefab6 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -880,10 +880,16 @@ class AppLocalizationsFr extends AppLocalizations { String get restore_defaults => 'Restaurer les valeurs par défaut'; @override - String get download_music_codec => 'Télécharger le codec musical'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Codec de musique en streaming'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Se connecter avec Last.fm'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 3c16bdfc..9a483e3a 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -872,10 +872,16 @@ class AppLocalizationsHi extends AppLocalizations { String get restore_defaults => 'डिफ़ॉल्ट सेटिंग्स को बहाल करें'; @override - String get download_music_codec => 'संगीत कोडेक डाउनलोड करें'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'स्ट्रीमिंग संगीत कोडेक'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm से लॉगिन करें'; diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index f1231523..68a078b4 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -874,10 +874,16 @@ class AppLocalizationsId extends AppLocalizations { String get restore_defaults => 'Kembalikan semula'; @override - String get download_music_codec => 'Unduh codec musik'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Streaming codec musik'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Masuk dengan Last.fm'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index c781bd31..2385e466 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -874,10 +874,16 @@ class AppLocalizationsIt extends AppLocalizations { String get restore_defaults => 'Ripristina default'; @override - String get download_music_codec => 'Codec musicale scaricamento'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Codec musicale streaming'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Accesso a Last.fm'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 525e6c66..73c283de 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -861,10 +861,16 @@ class AppLocalizationsJa extends AppLocalizations { String get restore_defaults => '設定を初期化'; @override - String get download_music_codec => 'ダウンロード用の音声コーデック'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'ストリーミング用の音声コーデック'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fmでログイン'; diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 3d960d7e..313aba60 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -872,10 +872,16 @@ class AppLocalizationsKa extends AppLocalizations { String get restore_defaults => 'ნაგულისხმევი პარამეტრების აღდგენა'; @override - String get download_music_codec => 'მუსიკის კოდეკის გადმოწერა'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'სტრიმინგ მუსიკის კოდეკი'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm-ით შესვლა'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 12a3a5e3..053bd3cf 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -862,10 +862,16 @@ class AppLocalizationsKo extends AppLocalizations { String get restore_defaults => '기본값으로 복원'; @override - String get download_music_codec => '다운로드 음악 코덱'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => '스트리밍 음악 코덱'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm에 로그인'; diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 64b3311e..5fe152ee 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -880,10 +880,16 @@ class AppLocalizationsNe extends AppLocalizations { String get restore_defaults => 'पूर्वनिर्धारितहरू पुनः स्थापित गर्नुहोस्'; @override - String get download_music_codec => 'साङ्गीत कोडेक डाउनलोड गर्नुहोस्'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'स्ट्रिमिङ साङ्गीत कोडेक'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'लास्ट.एफ.एम सँग लगइन गर्नुहोस्'; diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 78d34a27..c6585faf 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -872,10 +872,16 @@ class AppLocalizationsNl extends AppLocalizations { String get restore_defaults => 'Standaardwaarden herstellen'; @override - String get download_music_codec => 'Download-codec'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Streaming-codec'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Inloggen met Last.fm'; diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index afe29fca..88edf4c9 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -873,10 +873,16 @@ class AppLocalizationsPl extends AppLocalizations { String get restore_defaults => 'Przywróć domyślne'; @override - String get download_music_codec => 'Pobierz kodek muzyczny'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Kodek strumieniowy muzyki'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Zaloguj się z Last.fm'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 74ead955..2d948030 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -873,10 +873,16 @@ class AppLocalizationsPt extends AppLocalizations { String get restore_defaults => 'Restaurar padrões'; @override - String get download_music_codec => 'Descarregar codec de música'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Codec de streaming de música'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Iniciar sessão com o Last.fm'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index f6ad081e..29257248 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -874,10 +874,16 @@ class AppLocalizationsRu extends AppLocalizations { String get restore_defaults => 'Восстановить настройки по умолчанию'; @override - String get download_music_codec => 'Загрузить кодек для музыки'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Кодек потоковой передачи музыки'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Войти с помощью Last.fm'; diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 94719207..08bd253b 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -879,10 +879,16 @@ class AppLocalizationsTa extends AppLocalizations { String get restore_defaults => 'இயல்புநிலைகளை மீட்டமை'; @override - String get download_music_codec => 'இசை கோடெக்கை பதிவிறக்கு'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'இசை கோடெக்கை ஸ்ட்ரீம் செய்'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm உடன் உள்நுழைக'; diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 11d82dd6..0dc52d64 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -872,10 +872,16 @@ class AppLocalizationsTh extends AppLocalizations { String get restore_defaults => 'คืนค่าเริ่มต้น'; @override - String get download_music_codec => 'ดาวน์โหลดโคเดคเพลง'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'สตรีมมิ่งโคเดคเพลง'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'เข้าสู่ระบบด้วย Last.fm'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index ac8415f5..e35f9f04 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -878,10 +878,16 @@ class AppLocalizationsTl extends AppLocalizations { String get restore_defaults => 'Ibalik ang mga default'; @override - String get download_music_codec => 'Codec para sa pag-download ng musika'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Codec para sa pag-stream ng musika'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Mag-login gamit ang Last.fm'; diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index cd4f7122..281d6ae9 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -875,10 +875,16 @@ class AppLocalizationsTr extends AppLocalizations { String get restore_defaults => 'Varsayılanları geri yükle'; @override - String get download_music_codec => 'Müzik codec bileşenini indir'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Müzik codec\'i akışı'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Last.fm ile giriş yap'; diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index a6297b1e..66496a17 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -875,10 +875,16 @@ class AppLocalizationsUk extends AppLocalizations { String get restore_defaults => 'Відновити налаштування за замовчуванням'; @override - String get download_music_codec => 'Завантажити кодек для музики'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Кодек потокової передачі музики'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Увійти з Last.fm'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 1421c907..4a6bbafd 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -875,10 +875,16 @@ class AppLocalizationsVi extends AppLocalizations { String get restore_defaults => 'Khôi phục mặc định'; @override - String get download_music_codec => 'Định dạng tải xuống'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => 'Định dạng nghe'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => 'Đăng nhập bằng tài khoản Last.fm'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 6232965a..f1a25912 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -859,10 +859,16 @@ class AppLocalizationsZh extends AppLocalizations { String get restore_defaults => '恢复默认值'; @override - String get download_music_codec => '下载音乐编解码器'; + String get download_music_format => 'Download music format'; @override - String get streaming_music_codec => '流媒体音乐编解码器'; + String get streaming_music_format => 'Streaming music format'; + + @override + String get download_music_quality => 'Download music quality'; + + @override + String get streaming_music_quality => 'Streaming music quality'; @override String get login_with_lastfm => '使用 Last.fm 登录'; @@ -2376,12 +2382,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get restore_defaults => '恢復預設值'; - @override - String get download_music_codec => '下載音樂編解碼器'; - - @override - String get streaming_music_codec => '串流音樂編解碼器'; - @override String get login_with_lastfm => '使用 Last.fm 登入'; diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 7b780143..b9bd7631 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/ui/button_tile.dart'; @@ -9,8 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/audio_player/querying_track_info.dart'; -import 'package:spotube/provider/server/active_track_sources.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; class SiblingTracksSheet extends HookConsumerWidget { final bool floating; @@ -23,126 +23,133 @@ class SiblingTracksSheet extends HookConsumerWidget { Widget build(BuildContext context, ref) { final controller = useScrollController(); - final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final activeTrackSources = ref.watch(activeTrackSourcesProvider); - final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; - final activeTrack = activeTrackSources.asData?.value?.track; - final activeTrackSource = activeTrackSources.asData?.value?.source; + final activeTrack = + ref.watch(audioPlayerProvider.select((e) => e.activeTrack)); - final siblings = useMemoized>( - () => !isFetchingActiveTrack - ? [ - if (activeTrackSource != null) activeTrackSource.info, - ...?activeTrackSource?.siblings, - ] - : [], - [activeTrackSource, isFetchingActiveTrack], - ); + if (activeTrack == null || activeTrack is! SpotubeFullTrackObject) { + return const SafeArea(child: NotFound()); + } - final previousActiveTrack = usePrevious(activeTrack); - useEffect(() { - /// Populate sibling when active track changes - if (previousActiveTrack?.id == activeTrack?.id) return; - if (activeTrackSource != null && activeTrackSource.siblings.isEmpty) { - activeTrackNotifier?.copyWithSibling(); - } - return null; - }, [activeTrack, previousActiveTrack]); + return HookBuilder(builder: (context) { + final sourcedTrack = ref.watch(sourcedTrackProvider(activeTrack)); + final sourcedTrackNotifier = + ref.watch(sourcedTrackProvider(activeTrack).notifier); - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), - child: Row( - spacing: 5, - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Text( - context.l10n.alternative_track_sources, - ).bold()), - ], + final siblings = useMemoized>( + () => !sourcedTrack.isLoading + ? [ + if (sourcedTrack.asData?.value != null) + sourcedTrack.asData!.value.info, + ...?sourcedTrack.asData?.value.siblings, + ] + : [], + [sourcedTrack], + ); + + useEffect(() { + /// Populate sibling when active track changes + if (sourcedTrack.asData?.value != null && + sourcedTrack.asData?.value.siblings.isEmpty == true) { + sourcedTrackNotifier.copyWithSibling(); + } + return null; + }, [sourcedTrack]); + + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), + child: Row( + spacing: 5, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Text( + context.l10n.alternative_track_sources, + ).bold()), + ], + ), ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: activeTrackSources.isLoading - ? const SizedBox( - width: double.infinity, - child: LinearProgressIndicator(), - ) - : const SizedBox.shrink(), - ), - Expanded( - child: AnimatedSwitcher( + AnimatedSwitcher( duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: InterScrollbar( - controller: controller, - child: ListView.separated( - padding: const EdgeInsets.all(8.0), + child: sourcedTrack.isLoading + ? const SizedBox( + width: double.infinity, + child: LinearProgressIndicator(), + ) + : const SizedBox.shrink(), + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: InterScrollbar( controller: controller, - itemCount: siblings.length, - separatorBuilder: (context, index) => const Gap(8), - itemBuilder: (context, index) { - final sourceInfo = siblings[index]; + child: ListView.separated( + padding: const EdgeInsets.all(8.0), + controller: controller, + itemCount: siblings.length, + separatorBuilder: (context, index) => const Gap(8), + itemBuilder: (context, index) { + final sourceInfo = siblings[index]; - return ButtonTile( - style: ButtonVariance.ghost, - padding: const EdgeInsets.symmetric(horizontal: 8), - title: Text( - sourceInfo.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - leading: sourceInfo.thumbnail != null - ? UniversalImage( - path: sourceInfo.thumbnail!, - height: 60, - width: 60, - ) - : null, - trailing: - Text(sourceInfo.duration.toHumanReadableString()), - subtitle: Flexible( - child: Text( + return ButtonTile( + style: ButtonVariance.ghost, + padding: const EdgeInsets.symmetric(horizontal: 8), + title: Text( + sourceInfo.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + leading: sourceInfo.thumbnail != null + ? UniversalImage( + path: sourceInfo.thumbnail!, + height: 60, + width: 60, + ) + : null, + trailing: + Text(sourceInfo.duration.toHumanReadableString()), + subtitle: Text( sourceInfo.artists.join(", "), maxLines: 1, overflow: TextOverflow.ellipsis, ), - ), - enabled: !isFetchingActiveTrack, - selected: !isFetchingActiveTrack && - sourceInfo.id == activeTrackSource?.info.id, - onPressed: () async { - if (!isFetchingActiveTrack && - sourceInfo.id != activeTrackSource?.info.id) { - await activeTrackNotifier - ?.swapWithSibling(sourceInfo); - await ref - .read(audioPlayerProvider.notifier) - .swapActiveSource(); + enabled: !sourcedTrack.isLoading, + selected: !sourcedTrack.isLoading && + sourceInfo.id == sourcedTrack.asData?.value.info.id, + onPressed: () async { + if (!sourcedTrack.isLoading && + sourceInfo.id != + sourcedTrack.asData?.value.info.id) { + await sourcedTrackNotifier + .swapWithSibling(sourceInfo); + await ref + .read(audioPlayerProvider.notifier) + .swapActiveSource(); - if (context.mounted) { - if (MediaQuery.sizeOf(context).mdAndUp) { - closeOverlay(context); - } else { - closeDrawer(context); + if (context.mounted) { + if (MediaQuery.sizeOf(context).mdAndUp) { + closeOverlay(context); + } else { + closeDrawer(context); + } } } - } - }, - ); - }, + }, + ); + }, + ), ), ), ), - ), - ], - ), - ); + ], + ), + ); + }); } } diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 7a498ac0..0a29c991 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -67,8 +67,8 @@ class SettingsPlaybackSection extends HookConsumerWidget { ), if (sourcePresets.presets.isNotEmpty) ...[ AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: Text(context.l10n.streaming_music_codec), + secondary: const Icon(SpotubeIcons.plugin), + title: Text(context.l10n.streaming_music_format), value: sourcePresets.selectedStreamingContainerIndex, options: [ for (final MapEntry(:key, value: preset) @@ -81,8 +81,8 @@ class SettingsPlaybackSection extends HookConsumerWidget { }, ), AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: const Text("Streaming music quality"), + secondary: const Icon(SpotubeIcons.audioQuality), + title: Text(context.l10n.streaming_music_quality), value: sourcePresets.selectedStreamingQualityIndex, options: [ for (final MapEntry(:key, value: quality) in sourcePresets @@ -98,8 +98,8 @@ class SettingsPlaybackSection extends HookConsumerWidget { }, ), AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: Text(context.l10n.download_music_codec), + secondary: const Icon(SpotubeIcons.plugin), + title: Text(context.l10n.download_music_format), value: sourcePresets.selectedDownloadingContainerIndex, options: [ for (final MapEntry(:key, value: preset) @@ -112,8 +112,8 @@ class SettingsPlaybackSection extends HookConsumerWidget { }, ), AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.api), - title: const Text("Downloading music quality"), + secondary: const Icon(SpotubeIcons.audioQuality), + title: Text(context.l10n.download_music_quality), value: sourcePresets.selectedStreamingQualityIndex, options: [ for (final MapEntry(:key, value: quality) in sourcePresets diff --git a/lib/provider/history/summary.dart b/lib/provider/history/summary.dart index 99df4c11..5ced7559 100644 --- a/lib/provider/history/summary.dart +++ b/lib/provider/history/summary.dart @@ -53,7 +53,7 @@ class PlaybackHistorySummaryNotifier database.historyTable.itemId.count(distinct: true); final itemIdCountingCol = database.historyTable.itemId.count(); final durationSumJsonColumn = - database.historyTable.data.jsonExtract(r"$.duration_ms").sum(); + database.historyTable.data.jsonExtract(r"$.durationMs").sum(); final artistCountingCol = database.historyTable.data.jsonExtract(r"$.artists"); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 8f5a71fe..d1ecafdf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -56,9 +55,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) system_theme_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); - system_theme_plugin_register_with_registrar(system_theme_registrar); g_autoptr(FlPluginRegistrar) tray_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); tray_manager_plugin_register_with_registrar(tray_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1dd92b5c..fa74793f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST open_file_linux screen_retriever_linux sqlite3_flutter_libs - system_theme tray_manager url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2931c1b4..a7d025e8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -27,7 +27,6 @@ import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin import sqlite3_flutter_libs -import system_theme import tray_manager import url_launcher_macos import window_manager @@ -55,7 +54,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) - SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 85493ee5..9c423d76 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,7 +82,7 @@ packages: source: hosted version: "1.6.5" async: - dependency: "direct main" + dependency: transitive description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" @@ -531,14 +531,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.0" - dio_http2_adapter: - dependency: "direct main" - description: - name: dio_http2_adapter - sha256: b8bd5d587fd228a461711f8b82f378ccd4bf1fbf7802e7663ca60d7b5ce0e3aa - url: "https://pub.dev" - source: hosted - version: "2.6.0" dio_web_adapter: dependency: transitive description: @@ -1082,14 +1074,6 @@ packages: url: "https://pub.dev" source: hosted version: "11.2.0" - form_validator: - dependency: "direct main" - description: - name: form_validator - sha256: "8cbe91b7d5260870d6fb9e23acd55d5d1d1fdf2397f0279a4931ac3c0c7bf8fb" - url: "https://pub.dev" - source: hosted - version: "2.1.1" freezed: dependency: "direct dev" description: @@ -1128,7 +1112,7 @@ packages: source: hosted version: "1.2.0" gap: - dependency: "direct main" + dependency: transitive description: name: gap sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d @@ -1151,14 +1135,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: ebc94ed30fd13cefd397cb1658b593f21571f014b7d1197eeb41fb95f05d899a - url: "https://pub.dev" - source: hosted - version: "6.3.1" graphs: dependency: transitive description: @@ -1266,14 +1242,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - http2: - dependency: transitive - description: - name: http2 - sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" - url: "https://pub.dev" - source: hosted - version: "2.3.1" http_methods: dependency: transitive description: @@ -1424,7 +1392,7 @@ packages: source: hosted version: "0.6.7" json_annotation: - dependency: "direct main" + dependency: transitive description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" @@ -1530,58 +1498,65 @@ packages: media_kit: dependency: "direct main" description: - name: media_kit - sha256: "52a8e989babc431db0aa242f32a4a08e55f60662477ea09759a105d7cd6410da" - url: "https://pub.dev" - source: hosted + path: media_kit + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.2.1" media_kit_libs_android_audio: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_libs_android_audio - sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b" - url: "https://pub.dev" - source: hosted + path: "libs/android/media_kit_libs_android_audio" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.3.8" media_kit_libs_audio: dependency: "direct main" description: - name: media_kit_libs_audio - sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316" - url: "https://pub.dev" - source: hosted + path: "libs/universal/media_kit_libs_audio" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.0.7" media_kit_libs_ios_audio: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_libs_ios_audio - sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f" - url: "https://pub.dev" - source: hosted + path: "libs/ios/media_kit_libs_ios_audio" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.1.4" media_kit_libs_linux: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_libs_linux - sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" - url: "https://pub.dev" - source: hosted + path: "libs/linux/media_kit_libs_linux" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.2.1" media_kit_libs_macos_audio: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_libs_macos_audio - sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950" - url: "https://pub.dev" - source: hosted + path: "libs/macos/media_kit_libs_macos_audio" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.1.4" media_kit_libs_windows_audio: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit_libs_windows_audio - sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 - url: "https://pub.dev" - source: hosted + path: "libs/windows/media_kit_libs_windows_audio" + ref: HEAD + resolved-ref: d310049f24196250d876efb02b9ff56fa9ef5068 + url: "https://github.com/media-kit/media-kit" + source: git version: "1.0.9" menu_base: dependency: transitive @@ -1711,14 +1686,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" - otp_util: - dependency: "direct main" - description: - name: otp_util - sha256: dd8956c6472bacc3ffabe62c03f8a9782d1e5a5a3f2674420970f549d642b1cf - url: "https://pub.dev" - source: hosted - version: "1.0.2" package_config: dependency: transitive description: @@ -2381,14 +2348,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - stroke_text: - dependency: "direct main" - description: - name: stroke_text - sha256: "783fee071e3a3c5d3fe24011d7d776ce3cd64792e01b650c6b727ac3f38cb37b" - url: "https://pub.dev" - source: hosted - version: "0.0.3" sync_http: dependency: transitive description: @@ -2405,22 +2364,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" - system_theme: - dependency: "direct main" - description: - name: system_theme - sha256: "5f93485401689601d4636a695f99f7c70a30873ee68c1d95025d908a3386be7e" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - system_theme_web: - dependency: transitive - description: - name: system_theme_web - sha256: "900c92c5c050ce58048f241ef9a17e5cd8629808325a05b473dc62a6e99bae77" - url: "https://pub.dev" - source: hosted - version: "0.0.3" term_glyph: dependency: transitive description: @@ -2430,7 +2373,7 @@ packages: source: hosted version: "1.2.2" test: - dependency: "direct main" + dependency: "direct dev" description: name: test sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" diff --git a/pubspec.yaml b/pubspec.yaml index d48f65d0..a93cb456 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,6 @@ environment: dependencies: app_links: ^6.4.0 args: ^2.5.0 - async: ^2.11.0 audio_service: ^0.18.13 audio_service_mpris: ^0.2.0 audio_session: ^0.1.19 @@ -64,11 +63,8 @@ dependencies: flutter_sharing_intent: ^1.1.0 flutter_undraw: ^0.2.1 form_builder_validators: ^11.1.1 - form_validator: ^2.1.1 freezed_annotation: ^2.4.1 fuzzywuzzy: ^1.1.6 - gap: ^3.0.1 - google_fonts: ^6.2.1 home_widget: ^0.7.0 hooks_riverpod: ^2.5.1 html: ^0.15.1 @@ -76,13 +72,10 @@ dependencies: http: ^1.2.1 image_picker: ^1.1.0 intl: any - json_annotation: ^4.8.1 local_notifier: ^0.1.6 logger: ^2.0.2 logging: ^1.3.0 lrc: ^1.0.2 - media_kit: ^1.2.1 - media_kit_libs_audio: ^1.0.7 metadata_god: ^1.1.0 mime: ^2.0.0 open_file: ^3.5.10 @@ -109,9 +102,6 @@ dependencies: smtc_windows: ^1.1.0 sqlite3: ^2.4.3 sqlite3_flutter_libs: ^0.5.23 - stroke_text: ^0.0.2 - system_theme: ^3.1.2 - test: ^1.25.7 timezone: ^0.10.0 titlebar_buttons: ^1.0.0 tray_manager: ^0.5.0 @@ -134,8 +124,6 @@ dependencies: url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git http_parser: ^4.1.2 collection: any - otp_util: ^1.0.2 - dio_http2_adapter: ^2.6.0 archive: ^4.0.7 hetu_script: ^0.4.2+1 hetu_std: @@ -155,6 +143,15 @@ dependencies: pub_semver: ^2.2.0 change_case: ^1.1.0 flutter_secure_storage: ^9.2.4 + # Have to use the git version due to unresponsive .move() after .add() + media_kit: + git: + url: https://github.com/media-kit/media-kit + path: media_kit + media_kit_libs_audio: + git: + url: https://github.com/media-kit/media-kit + path: libs/universal/media_kit_libs_audio dev_dependencies: build_runner: ^2.4.13 @@ -173,6 +170,7 @@ dev_dependencies: pub_api_client: ^3.0.0 io: ^1.0.4 drift_dev: ^2.21.0 + test: ^1.25.7 auto_route_generator: ^9.0.0 dependency_overrides: @@ -191,6 +189,26 @@ dependency_overrides: url: https://github.com/m-berto/flutter_secure_storage.git ref: patch-2 path: flutter_secure_storage_linux + media_kit_libs_android_audio: + git: + url: https://github.com/media-kit/media-kit + path: libs/android/media_kit_libs_android_audio + media_kit_libs_ios_audio: + git: + url: https://github.com/media-kit/media-kit + path: libs/ios/media_kit_libs_ios_audio + media_kit_libs_macos_audio: + git: + url: https://github.com/media-kit/media-kit + path: libs/macos/media_kit_libs_macos_audio + media_kit_libs_windows_audio: + git: + url: https://github.com/media-kit/media-kit + path: libs/windows/media_kit_libs_windows_audio + media_kit_libs_linux: + git: + url: https://github.com/media-kit/media-kit + path: libs/linux/media_kit_libs_linux flutter: generate: true diff --git a/untranslated_messages.json b/untranslated_messages.json index bcde408d..2ca92876 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,5 +1,9 @@ { "ar": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -12,6 +16,10 @@ ], "bn": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -24,6 +32,10 @@ ], "ca": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -36,6 +48,10 @@ ], "cs": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -48,6 +64,10 @@ ], "de": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -60,6 +80,10 @@ ], "es": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -72,6 +96,10 @@ ], "eu": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -84,6 +112,10 @@ ], "fa": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -96,6 +128,10 @@ ], "fi": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -108,6 +144,10 @@ ], "fr": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -120,6 +160,10 @@ ], "hi": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -132,6 +176,10 @@ ], "id": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -144,6 +192,10 @@ ], "it": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -156,6 +208,10 @@ ], "ja": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -168,6 +224,10 @@ ], "ka": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -180,6 +240,10 @@ ], "ko": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -192,6 +256,10 @@ ], "ne": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -205,6 +273,10 @@ "nl": [ "audio_source", + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -217,6 +289,10 @@ ], "pl": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -229,6 +305,10 @@ ], "pt": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -241,6 +321,10 @@ ], "ru": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -253,6 +337,10 @@ ], "ta": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -265,6 +353,10 @@ ], "th": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -277,6 +369,10 @@ ], "tl": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -289,6 +385,10 @@ ], "tr": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -301,6 +401,10 @@ ], "uk": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -313,6 +417,10 @@ ], "vi": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -325,6 +433,10 @@ ], "zh": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", @@ -337,6 +449,10 @@ ], "zh_TW": [ + "download_music_format", + "streaming_music_format", + "download_music_quality", + "streaming_music_quality", "default_metadata_source", "set_default_metadata_source", "default_audio_source", diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 95f52491..2441d2e2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -54,8 +53,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); - SystemThemePluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SystemThemePlugin")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 51be79c2..9990b87b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -17,7 +17,6 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows screen_retriever_windows sqlite3_flutter_libs - system_theme tray_manager url_launcher_windows window_manager From a45212230296ab8eb0281c26fee49de2dae3d829 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Nov 2025 15:15:13 +0600 Subject: [PATCH 38/47] cd: add appimage support and use fastforge --- .github/Dockerfile | 25 ------------ .github/workflows/spotube-release-binary.yml | 3 +- cli/commands/build/common.dart | 2 +- cli/commands/build/linux.dart | 40 +++++++++----------- cli/commands/build/macos.dart | 2 +- cli/commands/build/windows.dart | 2 +- cli/commands/install-dependencies.dart | 7 +++- 7 files changed, 28 insertions(+), 53 deletions(-) delete mode 100644 .github/Dockerfile diff --git a/.github/Dockerfile b/.github/Dockerfile deleted file mode 100644 index f6a9f538..00000000 --- a/.github/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG FLUTTER_VERSION - -FROM --platform=linux/arm64 krtirtho/flutter_distributor:${FLUTTER_VERSION} - -ARG BUILD_VERSION - -WORKDIR /app - -COPY . . - -RUN chown -R $(whoami) /app - -RUN rustup target add aarch64-unknown-linux-gnu - -RUN flutter pub get - -RUN alias dpkg-deb="dpkg-deb --Zxz" &&\ - flutter_distributor package --platform=linux --targets=deb --skip-clean - -RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64 - -RUN mv build/spotube-linux-*-aarch64.tar.xz dist/ &&\ - mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb - -CMD [ "sleep", "5000000" ] \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 449165e6..52fa44f8 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -36,13 +36,14 @@ jobs: arch: x86 files: | dist/Spotube-linux-x86_64.deb - dist/Spotube-linux-x86_64.rpm + dist/Spotube-linux-x86_64.AppImage dist/spotube-linux-*-x86_64.tar.xz - os: ubuntu-22.04-arm platform: linux arch: arm64 files: | dist/Spotube-linux-aarch64.deb + dist/Spotube-linux-aarch64.AppImage dist/spotube-linux-*-aarch64.tar.xz - os: ubuntu-22.04 platform: android diff --git a/cli/commands/build/common.dart b/cli/commands/build/common.dart index 30906b3c..c30197f5 100644 --- a/cli/commands/build/common.dart +++ b/cli/commands/build/common.dart @@ -59,7 +59,7 @@ mixin BuildCommandCommonSteps on Command { """ flutter pub get dart run build_runner build --delete-conflicting-outputs - dart pub global activate flutter_distributor + dart pub global activate fastforge """, ); } diff --git a/cli/commands/build/linux.dart b/cli/commands/build/linux.dart index 378f5a72..84a7b6bb 100644 --- a/cli/commands/build/linux.dart +++ b/cli/commands/build/linux.dart @@ -37,15 +37,9 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps { await bootstrap(); await shell.run( - "flutter_distributor package --platform=linux --targets=deb", + "fastforge package --platform=linux --targets=deb,appimage", ); - if (architecture == "x86") { - await shell.run( - "flutter_distributor package --platform=linux --targets=rpm", - ); - } - final tempDir = join(Directory.systemTemp.path, "spotube-tar"); final bundleArchName = architecture == "x86" ? "x86_64" : "aarch64"; final bundleDirPath = join( @@ -99,22 +93,22 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps { ); await ogDeb.delete(); - if (architecture == "x86") { - final ogRpm = File( - join( - cwd.path, - "dist", - pubspec.version.toString(), - "spotube-${pubspec.version}-linux.rpm", - ), - ); - - await ogRpm.copy( - join(cwd.path, "dist", "Spotube-linux-$bundleArchName.rpm"), - ); - - await ogRpm.delete(); - } + final ogAppImage = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-linux.AppImage", + ), + ); + await ogAppImage.copy( + join( + cwd.path, + "dist", + "Spotube-linux-$bundleArchName.AppImage", + ), + ); + await ogAppImage.delete(); stdout.writeln("✅ Linux building done"); } diff --git a/cli/commands/build/macos.dart b/cli/commands/build/macos.dart index e8f34b77..936f1fc8 100644 --- a/cli/commands/build/macos.dart +++ b/cli/commands/build/macos.dart @@ -21,7 +21,7 @@ class MacosBuildCommand extends Command with BuildCommandCommonSteps { """ flutter build macos appdmg appdmg.json ${join(cwd.path, "build", "Spotube-macos-universal.dmg")} - flutter_distributor package --platform=macos --targets pkg --skip-clean + fastforge package --platform=macos --targets pkg --skip-clean """, ); diff --git a/cli/commands/build/windows.dart b/cli/commands/build/windows.dart index c44ed52f..1045c11c 100644 --- a/cli/commands/build/windows.dart +++ b/cli/commands/build/windows.dart @@ -61,7 +61,7 @@ class WindowsBuildCommand extends Command with BuildCommandCommonSteps { ); await shell.run( - "flutter_distributor package --platform=windows --targets=exe --skip-clean", + "fastforge package --platform=windows --targets=exe --skip-clean", ); final ogExe = File( diff --git a/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart index 336ffae7..56f679f1 100644 --- a/cli/commands/install-dependencies.dart +++ b/cli/commands/install-dependencies.dart @@ -37,6 +37,8 @@ class InstallDependenciesCommand extends Command { FutureOr? run() async { final shell = Shell(); + final arch = argResults?.option("arch") == "x86" ? "x86_64" : "aarch64"; + switch (argResults!.option("platform")) { case "windows": await shell.run( @@ -49,7 +51,10 @@ class InstallDependenciesCommand extends Command { await shell.run( """ sudo apt-get update -y - sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev + sudo apt-get install -y wget tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev + wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$arch.AppImage" + chmod +x appimagetool + sudo mv appimagetool /usr/local/bin/ """, ); break; From 3462e32a6ca9c9cbf8240eb3b96d357d206f52b8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Nov 2025 15:39:58 +0600 Subject: [PATCH 39/47] chore: add arch for install deps --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 52fa44f8..f7e9f808 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -108,7 +108,7 @@ jobs: - name: Install ${{matrix.platform}} dependencies run: | flutter pub get - dart cli/cli.dart install-dependencies --platform=${{matrix.platform}} + dart cli/cli.dart install-dependencies --platform=${{matrix.platform}} --arch=${{matrix.arch}} - name: Sign Apk if: ${{matrix.platform == 'android'}} From 5ea4df932fa9bed806b3662163a23d6c3f4c1d06 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 13 Nov 2025 16:03:08 +0600 Subject: [PATCH 40/47] cd: add back rpm for x86_64 --- .github/workflows/spotube-release-binary.yml | 1 + cli/commands/build/linux.dart | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index f7e9f808..dfec7d44 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -36,6 +36,7 @@ jobs: arch: x86 files: | dist/Spotube-linux-x86_64.deb + dist/Spotube-linux-x86_64.rpm dist/Spotube-linux-x86_64.AppImage dist/spotube-linux-*-x86_64.tar.xz - os: ubuntu-22.04-arm diff --git a/cli/commands/build/linux.dart b/cli/commands/build/linux.dart index 84a7b6bb..3ca792ea 100644 --- a/cli/commands/build/linux.dart +++ b/cli/commands/build/linux.dart @@ -39,6 +39,11 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps { await shell.run( "fastforge package --platform=linux --targets=deb,appimage", ); + if (architecture == "x86") { + await shell.run( + "fastforge package --platform=linux --targets=rpm", + ); + } final tempDir = join(Directory.systemTemp.path, "spotube-tar"); final bundleArchName = architecture == "x86" ? "x86_64" : "aarch64"; @@ -93,6 +98,23 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps { ); await ogDeb.delete(); + if (architecture == "x86") { + final ogRpm = File( + join( + cwd.path, + "dist", + pubspec.version.toString(), + "spotube-${pubspec.version}-linux.rpm", + ), + ); + + await ogRpm.copy( + join(cwd.path, "dist", "Spotube-linux-$bundleArchName.rpm"), + ); + + await ogRpm.delete(); + } + final ogAppImage = File( join( cwd.path, From 3f5291ec9278744171aaa92e5f0c0fd55278dd4a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 13:09:46 +0600 Subject: [PATCH 41/47] chore: upgrade hetu_std --- lib/provider/server/routes/playback.dart | 1 - pubspec.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index ef64481c..db6bf8f5 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' as dio_lib; import 'package:flutter/foundation.dart'; diff --git a/pubspec.lock b/pubspec.lock index 9c423d76..f47f35cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1198,7 +1198,7 @@ packages: description: path: "." ref: main - resolved-ref: "577ad115dce0514afc53e2b3ab7b96bcd88d3be3" + resolved-ref: "401fde426339cf8f1e00dee22cc95f64c3e60053" url: "https://github.com/hetu-community/hetu_std.git" source: git version: "1.0.0" From bf2eb0fface5f229ef5445e441f7c06209457ab3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 13:36:20 +0600 Subject: [PATCH 42/47] chore: bump version and generate CHANGELOG --- CHANGELOG.md | 24 + lib/l10n/app_ar.arb | 15 +- lib/l10n/app_bn.arb | 15 +- lib/l10n/app_ca.arb | 15 +- lib/l10n/app_cs.arb | 15 +- lib/l10n/app_de.arb | 15 +- lib/l10n/app_es.arb | 15 +- lib/l10n/app_eu.arb | 15 +- lib/l10n/app_fa.arb | 15 +- lib/l10n/app_fi.arb | 15 +- lib/l10n/app_fr.arb | 15 +- lib/l10n/app_hi.arb | 15 +- lib/l10n/app_id.arb | 15 +- lib/l10n/app_it.arb | 15 +- lib/l10n/app_ja.arb | 17 +- lib/l10n/app_ka.arb | 15 +- lib/l10n/app_ko.arb | 15 +- lib/l10n/app_ne.arb | 15 +- lib/l10n/app_nl.arb | 18 +- lib/l10n/app_pl.arb | 15 +- lib/l10n/app_pt.arb | 15 +- lib/l10n/app_ru.arb | 15 +- lib/l10n/app_ta.arb | 15 +- lib/l10n/app_th.arb | 15 +- lib/l10n/app_tl.arb | 15 +- lib/l10n/app_tr.arb | 15 +- lib/l10n/app_uk.arb | 15 +- lib/l10n/app_vi.arb | 15 +- lib/l10n/app_zh.arb | 15 +- lib/l10n/app_zh_TW.arb | 15 +- lib/l10n/generated/app_localizations_ar.dart | 27 +- lib/l10n/generated/app_localizations_bn.dart | 26 +- lib/l10n/generated/app_localizations_ca.dart | 30 +- lib/l10n/generated/app_localizations_cs.dart | 26 +- lib/l10n/generated/app_localizations_de.dart | 25 +- lib/l10n/generated/app_localizations_es.dart | 26 +- lib/l10n/generated/app_localizations_eu.dart | 27 +- lib/l10n/generated/app_localizations_fa.dart | 26 +- lib/l10n/generated/app_localizations_fi.dart | 26 +- lib/l10n/generated/app_localizations_fr.dart | 25 +- lib/l10n/generated/app_localizations_hi.dart | 26 +- lib/l10n/generated/app_localizations_id.dart | 26 +- lib/l10n/generated/app_localizations_it.dart | 27 +- lib/l10n/generated/app_localizations_ja.dart | 27 +- lib/l10n/generated/app_localizations_ka.dart | 27 +- lib/l10n/generated/app_localizations_ko.dart | 27 +- lib/l10n/generated/app_localizations_ne.dart | 28 +- lib/l10n/generated/app_localizations_nl.dart | 28 +- lib/l10n/generated/app_localizations_pl.dart | 26 +- lib/l10n/generated/app_localizations_pt.dart | 24 +- lib/l10n/generated/app_localizations_ru.dart | 27 +- lib/l10n/generated/app_localizations_ta.dart | 27 +- lib/l10n/generated/app_localizations_th.dart | 26 +- lib/l10n/generated/app_localizations_tl.dart | 28 +- lib/l10n/generated/app_localizations_tr.dart | 27 +- lib/l10n/generated/app_localizations_uk.dart | 28 +- lib/l10n/generated/app_localizations_vi.dart | 26 +- lib/l10n/generated/app_localizations_zh.dart | 67 ++- pubspec.yaml | 2 +- untranslated_messages.json | 467 +------------------ 60 files changed, 857 insertions(+), 862 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ae3f15..b8a5b0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [5.1.0](https://github.com/KRTirtho/spotube/compare/v5.0.0...v5.1.0) (2025-11-14) + +### Features + +- Show plugin source and set the only plugin as default if no plugin is there +- **playback**: Add dab music source +- **playback**: Add uncompressed flac playback support +- Add plugin audio source models and api service +- **plugins**: Filter plugins by abilities in plugins page and show abilities as badge +- Add setting default audio source support +- Move away from track source query and preferences audio quality and codec +- Add NewPipe support for desktop platforms +- Add default plugin loading capability +- **queue**: Add multi-select and bulk actions to queue ([#2839](https://github.com/KRTirtho/spotube/issues/2839)) +- **android**: Add 16KB page size support + +### Bug Fixes + +- Change plugin download directory to application support +- **playback**: Play next not working +- Downloaded tracks are not tagged with metadata +- Download not working in different devices and slow +- **playback**: Use stream instead of chunked serving of audio bytes + ## [5.0.0](https://github.com/KRTirtho/spotube/compare/v4.0.2...v5.0.0) (2025-09-08) ### Features diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 99af2097..f1997517 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -477,5 +477,18 @@ "available_plugins": "الإضافات المتوفّرة", "configure_your_own_metadata_plugin": "تهيئة مزوّد بيانات للقائمة/الألبوم/الفنان/المصدر خاص بك", "audio_scrobblers": "أجهزة تتبع الصوت", - "scrobbling": "التتبع" + "scrobbling": "التتبع", + "download_music_format": "تنسيق تنزيل الموسيقى", + "streaming_music_format": "تنسيق بث الموسيقى", + "download_music_quality": "جودة تنزيل الموسيقى", + "streaming_music_quality": "جودة بث الموسيقى", + "default_metadata_source": "مصدر البيانات الوصفية الافتراضي", + "set_default_metadata_source": "تعيين مصدر البيانات الوصفية الافتراضي", + "default_audio_source": "مصدر الصوت الافتراضي", + "set_default_audio_source": "تعيين مصدر الصوت الافتراضي", + "plugins": "الإضافات", + "configure_plugins": "قم بتكوين مزود البيانات الوصفية ومكونات مصدر الصوت الخاصة بك", + "source": "المصدر: ", + "uncompressed": "غير مضغوط", + "dab_music_source_description": "لمحبي الصوتيات. يوفر تدفقات صوتية عالية الجودة/بدون فقدان. مطابقة دقيقة للمسارات بناءً على ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 222e28b4..4d001da1 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -477,5 +477,18 @@ "available_plugins": "উপলব্ধ প্লাগইনগুলো", "configure_your_own_metadata_plugin": "নিজস্ব প্লেলিস্ট/অ্যালবাম/শিল্পী/ফিড মেটাডেটা প্রদানকারী কনফিগার করুন", "audio_scrobblers": "অডিও স্ক্রোব্বলার্স", - "scrobbling": "স্ক্রোব্বলিং" + "scrobbling": "স্ক্রোব্বলিং", + "download_music_format": "গান ডাউনলোডের বিন্যাস", + "streaming_music_format": "গান স্ট্রিমিং এর বিন্যাস", + "download_music_quality": "গান ডাউনলোডের মান", + "streaming_music_quality": "গান স্ট্রিমিং এর মান", + "default_metadata_source": "ডিফল্ট মেটাডেটা উৎস", + "set_default_metadata_source": "ডিফল্ট মেটাডেটা উৎস সেট করুন", + "default_audio_source": "ডিফল্ট অডিও উৎস", + "set_default_audio_source": "ডিফল্ট অডিও উৎস সেট করুন", + "plugins": "প্লাগইন", + "configure_plugins": "আপনার নিজের মেটাডেটা প্রদানকারী এবং অডিও উৎস প্লাগইন কনফিগার করুন", + "source": "উৎস: ", + "uncompressed": "অ-সংকুচিত", + "dab_music_source_description": "অডিওফাইলদের জন্য। উচ্চ-মানের/লসলেস অডিও স্ট্রিম প্রদান করে। সঠিক ISRC ভিত্তিক ট্র্যাক ম্যাচিং।" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 0482468b..06ed7ec6 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -477,5 +477,18 @@ "available_plugins": "Complements disponibles", "configure_your_own_metadata_plugin": "Configura el teu propi proveïdor de metadades per llistes/reproduccions àlbum/artista/flux", "audio_scrobblers": "Scrobblers d’àudio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Format de descàrrega de música", + "streaming_music_format": "Format de reproducció de música en temps real", + "download_music_quality": "Qualitat de descàrrega de música", + "streaming_music_quality": "Qualitat de reproducció de música en temps real", + "default_metadata_source": "Font de metadades per defecte", + "set_default_metadata_source": "Estableix la font de metadades per defecte", + "default_audio_source": "Font d'àudio per defecte", + "set_default_audio_source": "Estableix la font d'àudio per defecte", + "plugins": "Connectors", + "configure_plugins": "Configura els teus propis connectors de proveïdor de metadades i de font d'àudio", + "source": "Font: ", + "uncompressed": "Sense comprimir", + "dab_music_source_description": "Per als audiòfils. Ofereix fluxos d'àudio d'alta qualitat/sense pèrdua. Coincidència precisa de pistes basada en ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index d5d0f7af..59938004 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -477,5 +477,18 @@ "available_plugins": "Dostupné pluginy", "configure_your_own_metadata_plugin": "Nakonfigurujte si vlastního poskytovatele metadat pro playlist/album/umělec/fid", "audio_scrobblers": "Audio scrobblers", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Formát stahování hudby", + "streaming_music_format": "Formát streamování hudby", + "download_music_quality": "Kvalita stahování hudby", + "streaming_music_quality": "Kvalita streamování hudby", + "default_metadata_source": "Výchozí zdroj metadat", + "set_default_metadata_source": "Nastavit výchozí zdroj metadat", + "default_audio_source": "Výchozí zdroj zvuku", + "set_default_audio_source": "Nastavit výchozí zdroj zvuku", + "plugins": "Pluginy", + "configure_plugins": "Konfigurujte své vlastní pluginy poskytovatele metadat a zdroje zvuku", + "source": "Zdroj: ", + "uncompressed": "Nekomprimováno", + "dab_music_source_description": "Pro audiofily. Poskytuje vysoce kvalitní/bezztrátové zvukové toky. Přesná shoda skladeb na základě ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8ef22fac..458e7c07 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -477,5 +477,18 @@ "available_plugins": "Verfügbare Plugins", "configure_your_own_metadata_plugin": "Eigenen Anbieter für Playlist-/Album-/Künstler-/Feed-Metadaten konfigurieren", "audio_scrobblers": "Audio-Scrobbler", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Musik-Downloadformat", + "streaming_music_format": "Musik-Streamingformat", + "download_music_quality": "Musik-Downloadqualität", + "streaming_music_quality": "Musik-Streamingqualität", + "default_metadata_source": "Standard-Metadatenquelle", + "set_default_metadata_source": "Standard-Metadatenquelle festlegen", + "default_audio_source": "Standard-Audioquelle", + "set_default_audio_source": "Standard-Audioquelle festlegen", + "plugins": "Plugins", + "configure_plugins": "Richte deine eigenen Metadatenanbieter- und Audioquellen-Plugins ein", + "source": "Quelle: ", + "uncompressed": "Unkomprimiert", + "dab_music_source_description": "Für Audiophile. Bietet hochwertige/verlustfreie Audiostreams. Präzises ISRC-basiertes Track-Matching." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 8321f2f5..32822763 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -477,5 +477,18 @@ "available_plugins": "Complementos disponibles", "configure_your_own_metadata_plugin": "Configura tu propio proveedor de metadatos para listas/álbum/artista/feeds", "audio_scrobblers": "Scrobblers de audio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Formato de descarga de música", + "streaming_music_format": "Formato de transmisión de música", + "download_music_quality": "Calidad de descarga de música", + "streaming_music_quality": "Calidad de transmisión de música", + "default_metadata_source": "Fuente de metadatos predeterminada", + "set_default_metadata_source": "Establecer fuente de metadatos predeterminada", + "default_audio_source": "Fuente de audio predeterminada", + "set_default_audio_source": "Establecer fuente de audio predeterminada", + "plugins": "Plugins", + "configure_plugins": "Configura tus propios plugins de proveedor de metadatos y fuente de audio", + "source": "Fuente: ", + "uncompressed": "Sin comprimir", + "dab_music_source_description": "Para audiófilos. Proporciona transmisiones de audio de alta calidad/sin pérdida. Coincidencia precisa de pistas basada en ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 0b49bea1..8c87fd2c 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -477,5 +477,18 @@ "available_plugins": "Eskaintzen diren pluginak", "configure_your_own_metadata_plugin": "Konfiguratu zureko playlists-/album-/artista-/feed-metadaten hornitzailea", "audio_scrobblers": "Audio scrobbler-ak", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Musika deskargatzeko formatua", + "streaming_music_format": "Musika streaming bidezko formatua", + "download_music_quality": "Musika deskargaren kalitatea", + "streaming_music_quality": "Streaming bidezko musika kalitatea", + "default_metadata_source": "Metadatu-iturburu lehenetsia", + "set_default_metadata_source": "Ezarri metadatu-iturburu lehenetsia", + "default_audio_source": "Audio-iturburu lehenetsia", + "set_default_audio_source": "Ezarri audio-iturburu lehenetsia", + "plugins": "Pluginak", + "configure_plugins": "Konfiguratu zure metadatu-hornitzaile eta audio-iturburu pluginak", + "source": "Iturburua: ", + "uncompressed": "Konprimitu gabea", + "dab_music_source_description": "Audiozaleentzat. Kalitate handiko/galerarik gabeko audio-streamak eskaintzen ditu. ISRC oinarritutako pistaren parekatze zehatza." } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 73bb4d48..72f775fc 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -477,5 +477,18 @@ "available_plugins": "افزونه‌های موجود", "configure_your_own_metadata_plugin": "پیکربندی ارائه‌دهندهٔ متادیتا برای پلی‌لیست/آلبوم/هنرمند/فید به‌صورت سفارشی", "audio_scrobblers": "اسکراب‌بلرهای صوتی", - "scrobbling": "اسکراب‌بلینگ" + "scrobbling": "اسکراب‌بلینگ", + "download_music_format": "فرمت دانلود موسیقی", + "streaming_music_format": "فرمت پخش آنلاین موسیقی", + "download_music_quality": "کیفیت دانلود موسیقی", + "streaming_music_quality": "کیفیت پخش آنلاین موسیقی", + "default_metadata_source": "منبع پیش‌فرض فراداده", + "set_default_metadata_source": "تنظیم منبع پیش‌فرض فراداده", + "default_audio_source": "منبع پیش‌فرض صوت", + "set_default_audio_source": "تنظیم منبع پیش‌فرض صوت", + "plugins": "افزونه‌ها", + "configure_plugins": "افزونه‌های منبع صوت و ارائه‌دهنده فراداده خود را پیکربندی کنید", + "source": "منبع: ", + "uncompressed": "بدون فشرده‌سازی", + "dab_music_source_description": "مخصوص علاقه‌مندان صدا. ارائه‌دهنده استریم‌های باکیفیت/بدون افت. تطبیق دقیق آهنگ بر اساس ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 93c6c577..d92e5acf 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -477,5 +477,18 @@ "available_plugins": "Saatavilla olevat lisäosat", "configure_your_own_metadata_plugin": "Määritä oma soittolistan/albumin/artistin/syötteen metatietojen tarjoaja", "audio_scrobblers": "Äänen scrobblerit", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Musiikin latausmuoto", + "streaming_music_format": "Musiikin suoratoistomuoto", + "download_music_quality": "Musiikin latauslaatu", + "streaming_music_quality": "Musiikin suoratoistolaadun", + "default_metadata_source": "Oletusarvoinen metatietolähde", + "set_default_metadata_source": "Aseta oletusmetatietolähde", + "default_audio_source": "Oletusarvoinen äänilähde", + "set_default_audio_source": "Aseta oletusäänilähde", + "plugins": "Laajennukset", + "configure_plugins": "Määritä omat metatietojen tarjoaja- ja äänilähdelaajennukset", + "source": "Lähde: ", + "uncompressed": "Pakkaamaton", + "dab_music_source_description": "Audiofiileille. Tarjoaa korkealaatuisia/häviöttömiä äänivirtoja. Tarkka ISRC-pohjainen kappaleiden tunnistus." } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1851dbe1..e73c2eb2 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -478,5 +478,18 @@ "available_plugins": "Plugins disponibles", "configure_your_own_metadata_plugin": "Configurer votre propre fournisseur de métadonnées de playlist/album/artiste/flux", "audio_scrobblers": "Scrobblers audio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Format de téléchargement de musique", + "streaming_music_format": "Format de streaming de musique", + "download_music_quality": "Qualité de téléchargement de musique", + "streaming_music_quality": "Qualité de streaming de musique", + "default_metadata_source": "Source de métadonnées par défaut", + "set_default_metadata_source": "Définir la source de métadonnées par défaut", + "default_audio_source": "Source audio par défaut", + "set_default_audio_source": "Définir la source audio par défaut", + "plugins": "Plugins", + "configure_plugins": "Configurez vos propres plugins de fournisseur de métadonnées et de source audio", + "source": "Source : ", + "uncompressed": "Non compressé", + "dab_music_source_description": "Pour les audiophiles. Fournit des flux audio de haute qualité/sans perte. Correspondance précise des pistes basée sur ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index d0c6ba36..8e8087bb 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -477,5 +477,18 @@ "available_plugins": "उपलब्ध प्लगइन", "configure_your_own_metadata_plugin": "अपनी खुद की प्लेलिस्ट/एल्बम/कलाकार/फ़ीड मेटाडेटा प्रदाता कॉन्फ़िगर करें", "audio_scrobblers": "ऑडियो स्क्रॉबलर्स", - "scrobbling": "स्क्रॉबलिंग" + "scrobbling": "स्क्रॉबलिंग", + "download_music_format": "संगीत डाउनलोड प्रारूप", + "streaming_music_format": "संगीत स्ट्रीमिंग प्रारूप", + "download_music_quality": "संगीत डाउनलोड गुणवत्ता", + "streaming_music_quality": "संगीत स्ट्रीमिंग गुणवत्ता", + "default_metadata_source": "डिफ़ॉल्ट मेटाडेटा स्रोत", + "set_default_metadata_source": "डिफ़ॉल्ट मेटाडेटा स्रोत सेट करें", + "default_audio_source": "डिफ़ॉल्ट ऑडियो स्रोत", + "set_default_audio_source": "डिफ़ॉल्ट ऑडियो स्रोत सेट करें", + "plugins": "प्लगइन्स", + "configure_plugins": "अपने स्वयं के मेटाडेटा प्रदाता और ऑडियो स्रोत प्लगइन्स कॉन्फ़िगर करें", + "source": "स्रोत: ", + "uncompressed": "असंपीड़ित", + "dab_music_source_description": "ऑडियोफाइलों के लिए। उच्च-गुणवत्ता/बिना हानि वाले ऑडियो स्ट्रीम प्रदान करता है। सटीक ISRC आधारित ट्रैक मिलान।" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 8ab234a7..3405fd2f 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -477,5 +477,18 @@ "available_plugins": "Plugin yang tersedia", "configure_your_own_metadata_plugin": "Konfigurasi penyedia metadata playlist/album/artis/feed Anda sendiri", "audio_scrobblers": "Scrobblers Audio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Format unduh musik", + "streaming_music_format": "Format streaming musik", + "download_music_quality": "Kualitas unduh musik", + "streaming_music_quality": "Kualitas streaming musik", + "default_metadata_source": "Sumber metadata default", + "set_default_metadata_source": "Atur sumber metadata default", + "default_audio_source": "Sumber audio default", + "set_default_audio_source": "Atur sumber audio default", + "plugins": "Plugin", + "configure_plugins": "Konfigurasi plugin penyedia metadata dan sumber audio Anda sendiri", + "source": "Sumber: ", + "uncompressed": "Tidak terkompresi", + "dab_music_source_description": "Untuk audiophile. Menyediakan aliran audio berkualitas tinggi/tanpa kehilangan. Pencocokkan trek yang akurat berdasarkan ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f489db5d..c544dbf3 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -478,5 +478,18 @@ "available_plugins": "Plugin disponibili", "configure_your_own_metadata_plugin": "Configura il tuo provider di metadati per playlist/album/artista/feed", "audio_scrobblers": "Scrobbler audio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Formato download musica", + "streaming_music_format": "Formato streaming musica", + "download_music_quality": "Qualità download musica", + "streaming_music_quality": "Qualità streaming musica", + "default_metadata_source": "Fonte metadati predefinita", + "set_default_metadata_source": "Imposta fonte metadati predefinita", + "default_audio_source": "Fonte audio predefinita", + "set_default_audio_source": "Imposta fonte audio predefinita", + "plugins": "Plugin", + "configure_plugins": "Configura i tuoi plugin per fornitore metadati e fonte audio", + "source": "Fonte: ", + "uncompressed": "Non compresso", + "dab_music_source_description": "Per audiophile. Fornisce flussi audio di alta qualità/senza perdita. Abbinamento traccia accurato basato su ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 88fc51c3..991f56be 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -476,5 +476,18 @@ "available_plugins": "利用可能なプラグイン", "configure_your_own_metadata_plugin": "独自のプレイリスト/アルバム/アーティスト/フィードのメタデータプロバイダーを構成", "audio_scrobblers": "オーディオスクロッブラー", - "scrobbling": "Scrobbling" -} + "scrobbling": "Scrobbling", + "download_music_format": "音楽ダウンロード形式", + "streaming_music_format": "音楽ストリーミング形式", + "download_music_quality": "音楽ダウンロード品質", + "streaming_music_quality": "音楽ストリーミング品質", + "default_metadata_source": "デフォルトメタデータソース", + "set_default_metadata_source": "デフォルトメタデータソースを設定", + "default_audio_source": "デフォルトオーディオソース", + "set_default_audio_source": "デフォルトオーディオソースを設定", + "plugins": "プラグイン", + "configure_plugins": "独自のメタデータプロバイダーとオーディオソースプラグインを設定", + "source": "ソース: ", + "uncompressed": "非圧縮", + "dab_music_source_description": "オーディオファイル向け。高品質/ロスレスオーディオストリームを提供。正確なISRCベースのトラックマッチング。" +} \ No newline at end of file diff --git a/lib/l10n/app_ka.arb b/lib/l10n/app_ka.arb index 7aaa5cfb..6a0cb06c 100644 --- a/lib/l10n/app_ka.arb +++ b/lib/l10n/app_ka.arb @@ -477,5 +477,18 @@ "available_plugins": "ხელმისაწვდომი პლაგინები", "configure_your_own_metadata_plugin": "დააყენეთ თქვენი საკუთარი პლეილისტის/ალბომის/არტისტის/ფიდის მეტამონაცემების პროვაიდერი", "audio_scrobblers": "აუდიო სქრობლერები", - "scrobbling": "სქრობლინგი" + "scrobbling": "სქრობლინგი", + "download_music_format": "მუსიკის ჩამოტვირთვის ფორმატი", + "streaming_music_format": "სტრიმინგის მუსიკის ფორმატი", + "download_music_quality": "ჩამოტვირთვის ხარისხი", + "streaming_music_quality": "სტრიმინგის ხარისხი", + "default_metadata_source": "ნაგულისხმევი მეტამონაცემების წყარო", + "set_default_metadata_source": "ნაგულისხმევი მეტამონაცემების წყაროს დაყენება", + "default_audio_source": "ნაგულისხმევი აუდიო წყარო", + "set_default_audio_source": "ნაგულისხმევი აუდიო წყაროს დაყენება", + "plugins": "პლაგინები", + "configure_plugins": "თქვენი საკუთარი მეტამონაცემებისა და აუდიო წყაროს პლაგინების კონფიგურაცია", + "source": "წყარო: ", + "uncompressed": "შეუკუმშავი", + "dab_music_source_description": "აუდიოფილებისთვის. უზრუნველყოფს მაღალი ხარისხის/უკომპრესო აუდიო სტრიმებს. ზუსტი შესაბამისობა ISRC-ის მიხედვით." } \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index b38e35c5..70a68f8b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -478,5 +478,18 @@ "available_plugins": "사용 가능한 플러그인", "configure_your_own_metadata_plugin": "자신만의 플레이리스트/앨범/아티스트/피드 메타데이터 제공자 구성", "audio_scrobblers": "오디오 스크로블러", - "scrobbling": "스크로블링" + "scrobbling": "스크로블링", + "download_music_format": "다운로드 음악 포맷", + "streaming_music_format": "스트리밍 음악 포맷", + "download_music_quality": "다운로드 음질", + "streaming_music_quality": "스트리밍 음질", + "default_metadata_source": "기본 메타데이터 소스", + "set_default_metadata_source": "기본 메타데이터 소스 설정", + "default_audio_source": "기본 오디오 소스", + "set_default_audio_source": "기본 오디오 소스 설정", + "plugins": "플러그인", + "configure_plugins": "직접 메타데이터 제공자와 오디오 소스 플러그인을 구성하세요", + "source": "출처: ", + "uncompressed": "비압축", + "dab_music_source_description": "오디오파일을 위한 소스입니다. 고음질/무손실 오디오 스트림을 제공하며 ISRC 기반으로 정확한 트랙 매칭을 지원합니다." } \ No newline at end of file diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb index 1da053c4..874c28a5 100644 --- a/lib/l10n/app_ne.arb +++ b/lib/l10n/app_ne.arb @@ -477,5 +477,18 @@ "available_plugins": "उपलब्ध प्लगइनहरू", "configure_your_own_metadata_plugin": "तपाईंको आफ्नै प्लेलिस्ट/एल्बम/कलाकार/फिड मेटाडेटा प्रदायक कन्फिगर गर्नुहोस्", "audio_scrobblers": "अडियो स्क्रब्बलरहरू", - "scrobbling": "स्क्रब्बलिंग" + "scrobbling": "स्क्रब्बलिंग", + "download_music_format": "सङ्गीत डाउनलोड ढाँचा", + "streaming_music_format": "स्ट्रिमिङ सङ्गीत ढाँचा", + "download_music_quality": "डाउनलोड गुणस्तर", + "streaming_music_quality": "स्ट्रिमिङ गुणस्तर", + "default_metadata_source": "पूर्वनिर्धारित मेटाडाटा स्रोत", + "set_default_metadata_source": "पूर्वनिर्धारित मेटाडाटा स्रोत सेट गर्नुहोस्", + "default_audio_source": "पूर्वनिर्धारित अडियो स्रोत", + "set_default_audio_source": "पूर्वनिर्धारित अडियो स्रोत सेट गर्नुहोस्", + "plugins": "प्लगइनहरू", + "configure_plugins": "आफ्नै मेटाडाटा प्रदायक र अडियो स्रोत प्लगइनहरू कन्फिगर गर्नुहोस्", + "source": "स्रोत: ", + "uncompressed": "असंक्षिप्त", + "dab_music_source_description": "अडियोप्रेमीहरूका लागि। उच्च गुणस्तर/लसलेस अडियो स्ट्रिमहरू उपलब्ध गराउँछ। ISRC-मा आधारित सटीक ट्र्याक मिलान।" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 9395ec35..4d8deac1 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -477,5 +477,19 @@ "available_plugins": "Beschikbare plugins", "configure_your_own_metadata_plugin": "Configureer uw eigen metadata-aanbieder voor afspeellijst/album/artiest/feed", "audio_scrobblers": "Audioscrobblers", - "scrobbling": "Scrobbling" -} + "scrobbling": "Scrobbling", + "download_music_format": "Download muziekformaat", + "streaming_music_format": "Streaming muziekformaat", + "download_music_quality": "Downloadkwaliteit", + "streaming_music_quality": "Streamingkwaliteit", + "default_metadata_source": "Standaard metadata-bron", + "set_default_metadata_source": "Standaard metadata-bron instellen", + "default_audio_source": "Standaard audiobron", + "set_default_audio_source": "Standaard audiobron instellen", + "plugins": "Plug-ins", + "configure_plugins": "Configureer je eigen metadata- en audiobron-plug-ins", + "source": "Bron: ", + "uncompressed": "Ongecomprimeerd", + "dab_music_source_description": "Voor audiofielen. Biedt hoge kwaliteit/lossless audiostreams. Nauwkeurige trackmatching op basis van ISRC.", + "audio_source": "Audiobron" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index fac9070a..80da1e89 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -477,5 +477,18 @@ "available_plugins": "Dostępne wtyczki", "configure_your_own_metadata_plugin": "Skonfiguruj własnego dostawcę metadanych dla playlisty/albumu/artysty/kanału", "audio_scrobblers": "Scrobblery audio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Format pobierania muzyki", + "streaming_music_format": "Format strumieniowania muzyki", + "download_music_quality": "Jakość pobierania", + "streaming_music_quality": "Jakość strumieniowania", + "default_metadata_source": "Domyślne źródło metadanych", + "set_default_metadata_source": "Ustaw domyślne źródło metadanych", + "default_audio_source": "Domyślne źródło audio", + "set_default_audio_source": "Ustaw domyślne źródło audio", + "plugins": "Wtyczki", + "configure_plugins": "Skonfiguruj własne wtyczki dostawców metadanych i źródeł audio", + "source": "Źródło: ", + "uncompressed": "Nieskompresowany", + "dab_music_source_description": "Dla audiofilów. Oferuje strumienie audio wysokiej jakości/lossless. Precyzyjne dopasowanie utworów na podstawie ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 71e5ab55..fa7845c3 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -477,5 +477,18 @@ "available_plugins": "Plugins disponíveis", "configure_your_own_metadata_plugin": "Configure seu próprio provedor de metadados de playlist/álbum/artista/feed", "audio_scrobblers": "Scrobblers de áudio", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Formato de download de música", + "streaming_music_format": "Formato de streaming de música", + "download_music_quality": "Qualidade de download", + "streaming_music_quality": "Qualidade de streaming", + "default_metadata_source": "Fonte padrão de metadados", + "set_default_metadata_source": "Definir fonte padrão de metadados", + "default_audio_source": "Fonte de áudio padrão", + "set_default_audio_source": "Definir fonte de áudio padrão", + "plugins": "Plugins", + "configure_plugins": "Configure seus próprios plugins de provedores de metadados e fontes de áudio", + "source": "Fonte: ", + "uncompressed": "Não comprimido", + "dab_music_source_description": "Para audiófilos. Fornece streams de áudio de alta qualidade/sem perdas. Correspondência precisa de faixas baseada em ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index bc7586ed..2e864268 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -477,5 +477,18 @@ "available_plugins": "Доступные плагины", "configure_your_own_metadata_plugin": "Настройте свой собственный поставщик метаданных для плейлиста/альбома/артиста/ленты", "audio_scrobblers": "Аудио скробблеры", - "scrobbling": "Скробблинг" + "scrobbling": "Скробблинг", + "download_music_format": "Формат загрузки музыки", + "streaming_music_format": "Формат потоковой музыки", + "download_music_quality": "Качество загрузки", + "streaming_music_quality": "Качество стриминга", + "default_metadata_source": "Источник метаданных по умолчанию", + "set_default_metadata_source": "Задать источник метаданных по умолчанию", + "default_audio_source": "Источник аудио по умолчанию", + "set_default_audio_source": "Задать источник аудио по умолчанию", + "plugins": "Плагины", + "configure_plugins": "Настройте собственные плагины провайдеров метаданных и источников аудио", + "source": "Источник: ", + "uncompressed": "Несжатый", + "dab_music_source_description": "Для аудиофилов. Предоставляет высококачественные/lossless аудиопотоки. Точное совпадение треков по ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index 01991a39..6cea7b1a 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -475,5 +475,18 @@ "available_plugins": "கிடைக்கக்கூடிய பிளகின்கள்", "configure_your_own_metadata_plugin": "உங்கள் சொந்த பிளேலிஸ்ட்/ஆல்பம்/கலைஞர்/ஊட்ட மெட்டாடேட்டா வழங்குநரை உள்ளமைக்கவும்", "audio_scrobblers": "ஆடியோ ஸ்க்ரோப்ளர்கள்", - "scrobbling": "ஸ்க்ரோப்ளிங்" + "scrobbling": "ஸ்க்ரோப்ளிங்", + "download_music_format": "இசை பதிவிறக்க வடிவம்", + "streaming_music_format": "இசை ஸ்ட்ரீமிங் வடிவம்", + "download_music_quality": "பதிவிறக்க தரம்", + "streaming_music_quality": "ஸ்ட்ரீமிங் தரம்", + "default_metadata_source": "இயல்புநிலை மெட்டாடேட்டா மூலம்", + "set_default_metadata_source": "இயல்புநிலை மெட்டாடேட்டா மூலத்தை அமை", + "default_audio_source": "இயல்புநிலை ஆடியோ மூலம்", + "set_default_audio_source": "இயல்புநிலை ஆடியோ மூலத்தை அமை", + "plugins": "செருகுநிரல்கள்", + "configure_plugins": "உங்கள் சொந்த மெட்டாடேட்டா வழங்குநர் மற்றும் ஆடியோ மூல செருகுநிரல்களை அமைக்கவும்", + "source": "மூலம்: ", + "uncompressed": "அழுத்தப்படாத", + "dab_music_source_description": "ஆடியோஃபைல்களுக்காக. உயர்தர/லாஸ்லெஸ் ஆடியோ ஸ்ட்ரீம்களை வழங்குகிறது. ISRC அடிப்படையில் துல்லியமான பாடல் பொருத்தம்." } \ No newline at end of file diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 4b999fe0..4f2efc0e 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -478,5 +478,18 @@ "available_plugins": "ปลั๊กอินที่มีอยู่", "configure_your_own_metadata_plugin": "กำหนดค่าผู้ให้บริการเมตาดาต้าเพลย์ลิสต์/อัลบั้ม/ศิลปิน/ฟีดของคุณเอง", "audio_scrobblers": "เครื่อง scrobbler เสียง", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "รูปแบบการดาวน์โหลดเพลง", + "streaming_music_format": "รูปแบบการสตรีมเพลง", + "download_music_quality": "คุณภาพการดาวน์โหลด", + "streaming_music_quality": "คุณภาพการสตรีม", + "default_metadata_source": "แหล่งเมตาดาต้าพื้นฐาน", + "set_default_metadata_source": "ตั้งค่าแหล่งเมตาดาต้าพื้นฐาน", + "default_audio_source": "แหล่งเสียงพื้นฐาน", + "set_default_audio_source": "ตั้งค่าแหล่งเสียงพื้นฐาน", + "plugins": "ปลั๊กอิน", + "configure_plugins": "กำหนดค่าปลั๊กอินผู้ให้บริการเมตาดาต้าและแหล่งเสียงของคุณเอง", + "source": "แหล่งที่มา: ", + "uncompressed": "ไม่บีบอัด", + "dab_music_source_description": "สำหรับคนรักเสียงเพลง ให้สตรีมเสียงคุณภาพสูง/ไร้การสูญเสียการบีบอัด การจับคู่แทร็กแม่นยำตาม ISRC" } \ No newline at end of file diff --git a/lib/l10n/app_tl.arb b/lib/l10n/app_tl.arb index 45e0b070..bf1f174c 100644 --- a/lib/l10n/app_tl.arb +++ b/lib/l10n/app_tl.arb @@ -475,5 +475,18 @@ "available_plugins": "Mga available na plugin", "configure_your_own_metadata_plugin": "I-configure ang iyong sariling playlist/album/artist/feed metadata provider", "audio_scrobblers": "Mga Audio Scrobbler", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "I-download na format ng musika", + "streaming_music_format": "Format ng streaming ng musika", + "download_music_quality": "Kalidad ng i-download na musika", + "streaming_music_quality": "Kalidad ng streaming ng musika", + "default_metadata_source": "Default na pinagmulan ng metadata", + "set_default_metadata_source": "Itakda ang default na pinagmulan ng metadata", + "default_audio_source": "Default na pinagmulan ng audio", + "set_default_audio_source": "Itakda ang default na pinagmulan ng audio", + "plugins": "Mga plugin", + "configure_plugins": "I-configure ang sarili mong metadata provider at mga audio source plugin", + "source": "Pinagmulan: ", + "uncompressed": "Hindi naka-compress", + "dab_music_source_description": "Para sa mga audiophile. Nagbibigay ng de-kalidad/walang loss na audio streams. Tumpak na pagtutugma ng track batay sa ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index ff5785dd..72734d3b 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -477,5 +477,18 @@ "available_plugins": "Mevcut eklentiler", "configure_your_own_metadata_plugin": "Kendi çalma listenizi/albümünüzü/sanatçınızı/akış meta veri sağlayıcınızı yapılandırın", "audio_scrobblers": "Ses Scrobbler'lar", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Müzik indirme formatı", + "streaming_music_format": "Müzik akış formatı", + "download_music_quality": "İndirilen müzik kalitesi", + "streaming_music_quality": "Yayınlanan müzik kalitesi", + "default_metadata_source": "Varsayılan meta veri kaynağı", + "set_default_metadata_source": "Varsayılan meta veri kaynağını ayarla", + "default_audio_source": "Varsayılan ses kaynağı", + "set_default_audio_source": "Varsayılan ses kaynağını ayarla", + "plugins": "Eklentiler", + "configure_plugins": "Kendi meta veri sağlayıcı ve ses kaynağı eklentilerinizi yapılandırın", + "source": "Kaynak: ", + "uncompressed": "Sıkıştırılmamış", + "dab_music_source_description": "Audiophile'ler için. Yüksek kaliteli/kayıpsız ses akışları sağlar. Doğru ISRC tabanlı parça eşleştirme." } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 8eb5a8e9..bdb723ad 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -477,5 +477,18 @@ "available_plugins": "Доступні плагіни", "configure_your_own_metadata_plugin": "Налаштуйте свій власний провайдер метаданих для плейлиста/альбому/виконавця/стрічки", "audio_scrobblers": "Аудіо скробблери", - "scrobbling": "Скроблінг" + "scrobbling": "Скроблінг", + "download_music_format": "Формат завантаження музики", + "streaming_music_format": "Формат потокової музики", + "download_music_quality": "Якість завантаженої музики", + "streaming_music_quality": "Якість потокової музики", + "default_metadata_source": "Джерело метаданих за замовчуванням", + "set_default_metadata_source": "Встановити джерело метаданих за замовчуванням", + "default_audio_source": "Джерело аудіо за замовчуванням", + "set_default_audio_source": "Встановити джерело аудіо за замовчуванням", + "plugins": "Плагіни", + "configure_plugins": "Налаштуйте власні плагіни метаданих і аудіоджерела", + "source": "Джерело: ", + "uncompressed": "Без стиснення", + "dab_music_source_description": "Для аудіофілів. Забезпечує високоякісні/без втрат аудіопотоки. Точна відповідність треків на основі ISRC." } \ No newline at end of file diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 453e12e9..5733963e 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -477,5 +477,18 @@ "available_plugins": "Các plugin có sẵn", "configure_your_own_metadata_plugin": "Cấu hình nhà cung cấp siêu dữ liệu danh sách phát/album/nghệ sĩ/nguồn cấp dữ liệu của riêng bạn", "audio_scrobblers": "Bộ scrobbler âm thanh", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "Định dạng nhạc tải về", + "streaming_music_format": "Định dạng nhạc phát trực tuyến", + "download_music_quality": "Chất lượng nhạc tải về", + "streaming_music_quality": "Chất lượng nhạc phát trực tuyến", + "default_metadata_source": "Nguồn siêu dữ liệu mặc định", + "set_default_metadata_source": "Đặt nguồn siêu dữ liệu mặc định", + "default_audio_source": "Nguồn âm thanh mặc định", + "set_default_audio_source": "Đặt nguồn âm thanh mặc định", + "plugins": "Tiện ích bổ sung", + "configure_plugins": "Cấu hình nhà cung cấp siêu dữ liệu và tiện ích nguồn âm thanh riêng", + "source": "Nguồn: ", + "uncompressed": "Không nén", + "dab_music_source_description": "Dành cho người yêu âm nhạc chất lượng cao. Cung cấp luồng âm thanh chất lượng cao/không nén. Phù hợp bài hát dựa trên ISRC chính xác." } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index cccb3214..44f7d38c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -477,5 +477,18 @@ "available_plugins": "可用插件", "configure_your_own_metadata_plugin": "配置您自己的播放列表/专辑/艺人/订阅元数据提供者", "audio_scrobblers": "音频 Scrobblers", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "下载音乐格式", + "streaming_music_format": "流媒体音乐格式", + "download_music_quality": "下载音乐质量", + "streaming_music_quality": "流媒体音乐质量", + "default_metadata_source": "默认元数据源", + "set_default_metadata_source": "设置默认元数据源", + "default_audio_source": "默认音频源", + "set_default_audio_source": "设置默认音频源", + "plugins": "插件", + "configure_plugins": "配置您自己的元数据提供者和音频源插件", + "source": "来源:", + "uncompressed": "无损", + "dab_music_source_description": "适合发烧友。提供高质量/无损音频流。基于 ISRC 的精确曲目匹配。" } \ No newline at end of file diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index fa4c3e67..934006d5 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -477,5 +477,18 @@ "available_plugins": "可用的外掛程式", "configure_your_own_metadata_plugin": "設定您自己的播放清單/專輯/藝人/動態中繼資料供應商", "audio_scrobblers": "音訊 Scrobblers", - "scrobbling": "Scrobbling" + "scrobbling": "Scrobbling", + "download_music_format": "下載音樂格式", + "streaming_music_format": "串流音樂格式", + "download_music_quality": "下載音樂品質", + "streaming_music_quality": "串流音樂品質", + "default_metadata_source": "預設中繼資料來源", + "set_default_metadata_source": "設定預設中繼資料來源", + "default_audio_source": "預設音訊來源", + "set_default_audio_source": "設定預設音訊來源", + "plugins": "外掛程式", + "configure_plugins": "配置您自己的中繼資料提供者和音訊來源外掛程式", + "source": "來源:", + "uncompressed": "未壓縮", + "dab_music_source_description": "適合音響發燒友。提供高品質/無損音訊串流。精確的 ISRC 曲目比對。" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 5f17edb0..8fd50ffa 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -874,16 +874,16 @@ class AppLocalizationsAr extends AppLocalizations { String get restore_defaults => 'استعادة الإعدادات الافتراضية'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'تنسيق تنزيل الموسيقى'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'تنسيق بث الموسيقى'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'جودة تنزيل الموسيقى'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'جودة بث الموسيقى'; @override String get login_with_lastfm => 'تسجيل الدخول باستخدام Last.fm'; @@ -1449,16 +1449,17 @@ class AppLocalizationsAr extends AppLocalizations { 'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'مصدر البيانات الوصفية الافتراضي'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'تعيين مصدر البيانات الوصفية الافتراضي'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'مصدر الصوت الافتراضي'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'تعيين مصدر الصوت الافتراضي'; @override String get set_default => 'تعيين كافتراضي'; @@ -1519,7 +1520,7 @@ class AppLocalizationsAr extends AppLocalizations { 'المدخل لا يتوافق مع التنسيق المطلوب'; @override - String get plugins => 'Plugins'; + String get plugins => 'الإضافات'; @override String get paste_plugin_download_url => @@ -1545,7 +1546,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'قم بتكوين مزود البيانات الوصفية ومكونات مصدر الصوت الخاصة بك'; @override String get audio_scrobblers => 'أجهزة تتبع الصوت'; @@ -1554,12 +1555,12 @@ class AppLocalizationsAr extends AppLocalizations { String get scrobbling => 'التتبع'; @override - String get source => 'Source: '; + String get source => 'المصدر: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'غير مضغوط'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'لمحبي الصوتيات. يوفر تدفقات صوتية عالية الجودة/بدون فقدان. مطابقة دقيقة للمسارات بناءً على ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_bn.dart b/lib/l10n/generated/app_localizations_bn.dart index 7967d63f..7dc1e07f 100644 --- a/lib/l10n/generated/app_localizations_bn.dart +++ b/lib/l10n/generated/app_localizations_bn.dart @@ -873,16 +873,16 @@ class AppLocalizationsBn extends AppLocalizations { String get restore_defaults => 'ডিফল্ট সেটিংস পুনরুদ্ধার করুন'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'গান ডাউনলোডের বিন্যাস'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'গান স্ট্রিমিং এর বিন্যাস'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'গান ডাউনলোডের মান'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'গান স্ট্রিমিং এর মান'; @override String get login_with_lastfm => 'Last.fm দিয়ে লগইন করুন'; @@ -1449,16 +1449,16 @@ class AppLocalizationsBn extends AppLocalizations { 'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'ডিফল্ট মেটাডেটা উৎস'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'ডিফল্ট মেটাডেটা উৎস সেট করুন'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'ডিফল্ট অডিও উৎস'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'ডিফল্ট অডিও উৎস সেট করুন'; @override String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন'; @@ -1520,7 +1520,7 @@ class AppLocalizationsBn extends AppLocalizations { 'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না'; @override - String get plugins => 'Plugins'; + String get plugins => 'প্লাগইন'; @override String get paste_plugin_download_url => @@ -1546,7 +1546,7 @@ class AppLocalizationsBn extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'আপনার নিজের মেটাডেটা প্রদানকারী এবং অডিও উৎস প্লাগইন কনফিগার করুন'; @override String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স'; @@ -1555,12 +1555,12 @@ class AppLocalizationsBn extends AppLocalizations { String get scrobbling => 'স্ক্রোব্বলিং'; @override - String get source => 'Source: '; + String get source => 'উৎস: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'অ-সংকুচিত'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'অডিওফাইলদের জন্য। উচ্চ-মানের/লসলেস অডিও স্ট্রিম প্রদান করে। সঠিক ISRC ভিত্তিক ট্র্যাক ম্যাচিং।'; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index 5b6a3138..a4f587c9 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -876,16 +876,18 @@ class AppLocalizationsCa extends AppLocalizations { String get restore_defaults => 'Restaura els valors per defecte'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Format de descàrrega de música'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => + 'Format de reproducció de música en temps real'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Qualitat de descàrrega de música'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => + 'Qualitat de reproducció de música en temps real'; @override String get login_with_lastfm => 'Inicia la sessió amb Last.fm'; @@ -1456,16 +1458,18 @@ class AppLocalizationsCa extends AppLocalizations { 'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Font de metadades per defecte'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Estableix la font de metadades per defecte'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Font d\'àudio per defecte'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => + 'Estableix la font d\'àudio per defecte'; @override String get set_default => 'Establir com a predeterminat'; @@ -1529,7 +1533,7 @@ class AppLocalizationsCa extends AppLocalizations { 'L’entrada no coincideix amb el format requerit'; @override - String get plugins => 'Plugins'; + String get plugins => 'Connectors'; @override String get paste_plugin_download_url => @@ -1555,7 +1559,7 @@ class AppLocalizationsCa extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configura els teus propis connectors de proveïdor de metadades i de font d\'àudio'; @override String get audio_scrobblers => 'Scrobblers d’àudio'; @@ -1564,12 +1568,12 @@ class AppLocalizationsCa extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Font: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Sense comprimir'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Per als audiòfils. Ofereix fluxos d\'àudio d\'alta qualitat/sense pèrdua. Coincidència precisa de pistes basada en ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_cs.dart b/lib/l10n/generated/app_localizations_cs.dart index 0f28b4ef..24d5b34b 100644 --- a/lib/l10n/generated/app_localizations_cs.dart +++ b/lib/l10n/generated/app_localizations_cs.dart @@ -869,16 +869,16 @@ class AppLocalizationsCs extends AppLocalizations { String get restore_defaults => 'Obnovit výchozí'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Formát stahování hudby'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Formát streamování hudby'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Kvalita stahování hudby'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Kvalita streamování hudby'; @override String get login_with_lastfm => 'Přihlásit se pomocí Last.fm'; @@ -1448,16 +1448,16 @@ class AppLocalizationsCs extends AppLocalizations { 'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Výchozí zdroj metadat'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Nastavit výchozí zdroj metadat'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Výchozí zdroj zvuku'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Nastavit výchozí zdroj zvuku'; @override String get set_default => 'Nastavit jako výchozí'; @@ -1520,7 +1520,7 @@ class AppLocalizationsCs extends AppLocalizations { 'Vstup neodpovídá požadovanému formátu'; @override - String get plugins => 'Plugins'; + String get plugins => 'Pluginy'; @override String get paste_plugin_download_url => @@ -1546,7 +1546,7 @@ class AppLocalizationsCs extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Konfigurujte své vlastní pluginy poskytovatele metadat a zdroje zvuku'; @override String get audio_scrobblers => 'Audio scrobblers'; @@ -1555,12 +1555,12 @@ class AppLocalizationsCs extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Zdroj: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Nekomprimováno'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Pro audiofily. Poskytuje vysoce kvalitní/bezztrátové zvukové toky. Přesná shoda skladeb na základě ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index b5b21b86..4ab10266 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -879,16 +879,16 @@ class AppLocalizationsDe extends AppLocalizations { String get restore_defaults => 'Standardeinstellungen wiederherstellen'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Musik-Downloadformat'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Musik-Streamingformat'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Musik-Downloadqualität'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Musik-Streamingqualität'; @override String get login_with_lastfm => 'Mit Last.fm anmelden'; @@ -1461,16 +1461,17 @@ class AppLocalizationsDe extends AppLocalizations { 'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Standard-Metadatenquelle'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Standard-Metadatenquelle festlegen'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Standard-Audioquelle'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Standard-Audioquelle festlegen'; @override String get set_default => 'Als Standard festlegen'; @@ -1558,7 +1559,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Richte deine eigenen Metadatenanbieter- und Audioquellen-Plugins ein'; @override String get audio_scrobblers => 'Audio-Scrobbler'; @@ -1567,12 +1568,12 @@ class AppLocalizationsDe extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Quelle: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Unkomprimiert'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Für Audiophile. Bietet hochwertige/verlustfreie Audiostreams. Präzises ISRC-basiertes Track-Matching.'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index ed595693..0fcd6739 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -876,16 +876,16 @@ class AppLocalizationsEs extends AppLocalizations { String get restore_defaults => 'Restaurar valores predeterminados'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Formato de descarga de música'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Formato de transmisión de música'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Calidad de descarga de música'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Calidad de transmisión de música'; @override String get login_with_lastfm => 'Iniciar sesión con Last.fm'; @@ -1458,16 +1458,18 @@ class AppLocalizationsEs extends AppLocalizations { 'Este complemento scrobblea tu música para generar tu historial de reproducción.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Fuente de metadatos predeterminada'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Establecer fuente de metadatos predeterminada'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Fuente de audio predeterminada'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => + 'Establecer fuente de audio predeterminada'; @override String get set_default => 'Establecer como predeterminado'; @@ -1558,7 +1560,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configura tus propios plugins de proveedor de metadatos y fuente de audio'; @override String get audio_scrobblers => 'Scrobblers de audio'; @@ -1567,12 +1569,12 @@ class AppLocalizationsEs extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Fuente: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Sin comprimir'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Para audiófilos. Proporciona transmisiones de audio de alta calidad/sin pérdida. Coincidencia precisa de pistas basada en ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index dbc52fdb..5f80397e 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -876,16 +876,16 @@ class AppLocalizationsEu extends AppLocalizations { String get restore_defaults => 'Berrezarri berezko balioak'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Musika deskargatzeko formatua'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Musika streaming bidezko formatua'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Musika deskargaren kalitatea'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Streaming bidezko musika kalitatea'; @override String get login_with_lastfm => 'Hasi saioa Last.fm-n'; @@ -1457,16 +1457,17 @@ class AppLocalizationsEu extends AppLocalizations { 'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Metadatu-iturburu lehenetsia'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Ezarri metadatu-iturburu lehenetsia'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Audio-iturburu lehenetsia'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Ezarri audio-iturburu lehenetsia'; @override String get set_default => 'Lehenetsi gisa ezarri'; @@ -1530,7 +1531,7 @@ class AppLocalizationsEu extends AppLocalizations { 'Sarrera ezin da beharrezko formatutik desberdina izan'; @override - String get plugins => 'Plugins'; + String get plugins => 'Pluginak'; @override String get paste_plugin_download_url => @@ -1556,7 +1557,7 @@ class AppLocalizationsEu extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Konfiguratu zure metadatu-hornitzaile eta audio-iturburu pluginak'; @override String get audio_scrobblers => 'Audio scrobbler-ak'; @@ -1565,12 +1566,12 @@ class AppLocalizationsEu extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Iturburua: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Konprimitu gabea'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Audiozaleentzat. Kalitate handiko/galerarik gabeko audio-streamak eskaintzen ditu. ISRC oinarritutako pistaren parekatze zehatza.'; } diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index ecd3a5c1..5c0b7c2b 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -870,16 +870,16 @@ class AppLocalizationsFa extends AppLocalizations { String get restore_defaults => 'بازیابی پیش فرض ها'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'فرمت دانلود موسیقی'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'فرمت پخش آنلاین موسیقی'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'کیفیت دانلود موسیقی'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'کیفیت پخش آنلاین موسیقی'; @override String get login_with_lastfm => 'ورود با Last.fm'; @@ -1447,16 +1447,16 @@ class AppLocalizationsFa extends AppLocalizations { 'این افزونه موسیقی شما را اسکراب می‌کند تا تاریخچهٔ شنیداری‌تان را تولید کند.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'منبع پیش‌فرض فراداده'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'تنظیم منبع پیش‌فرض فراداده'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'منبع پیش‌فرض صوت'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'تنظیم منبع پیش‌فرض صوت'; @override String get set_default => 'تنظیم به عنوان پیش‌فرض'; @@ -1518,7 +1518,7 @@ class AppLocalizationsFa extends AppLocalizations { 'ورودی با قالب مورد نیاز تطابق ندارد'; @override - String get plugins => 'Plugins'; + String get plugins => 'افزونه‌ها'; @override String get paste_plugin_download_url => @@ -1544,7 +1544,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'افزونه‌های منبع صوت و ارائه‌دهنده فراداده خود را پیکربندی کنید'; @override String get audio_scrobblers => 'اسکراب‌بلرهای صوتی'; @@ -1553,12 +1553,12 @@ class AppLocalizationsFa extends AppLocalizations { String get scrobbling => 'اسکراب‌بلینگ'; @override - String get source => 'Source: '; + String get source => 'منبع: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'بدون فشرده‌سازی'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'مخصوص علاقه‌مندان صدا. ارائه‌دهنده استریم‌های باکیفیت/بدون افت. تطبیق دقیق آهنگ بر اساس ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index bec4cbea..3f616849 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -872,16 +872,16 @@ class AppLocalizationsFi extends AppLocalizations { String get restore_defaults => 'Palauta oletukset'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Musiikin latausmuoto'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Musiikin suoratoistomuoto'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Musiikin latauslaatu'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Musiikin suoratoistolaadun'; @override String get login_with_lastfm => 'Kirjaudu sisään Last.fm:llä'; @@ -1449,16 +1449,16 @@ class AppLocalizationsFi extends AppLocalizations { 'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Oletusarvoinen metatietolähde'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Aseta oletusmetatietolähde'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Oletusarvoinen äänilähde'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Aseta oletusäänilähde'; @override String get set_default => 'Aseta oletukseksi'; @@ -1518,7 +1518,7 @@ class AppLocalizationsFi extends AppLocalizations { String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa'; @override - String get plugins => 'Plugins'; + String get plugins => 'Laajennukset'; @override String get paste_plugin_download_url => @@ -1544,7 +1544,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Määritä omat metatietojen tarjoaja- ja äänilähdelaajennukset'; @override String get audio_scrobblers => 'Äänen scrobblerit'; @@ -1553,12 +1553,12 @@ class AppLocalizationsFi extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Lähde: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Pakkaamaton'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Audiofiileille. Tarjoaa korkealaatuisia/häviöttömiä äänivirtoja. Tarkka ISRC-pohjainen kappaleiden tunnistus.'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index e6fefab6..3637391b 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -880,16 +880,16 @@ class AppLocalizationsFr extends AppLocalizations { String get restore_defaults => 'Restaurer les valeurs par défaut'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Format de téléchargement de musique'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Format de streaming de musique'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Qualité de téléchargement de musique'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Qualité de streaming de musique'; @override String get login_with_lastfm => 'Se connecter avec Last.fm'; @@ -1463,16 +1463,17 @@ class AppLocalizationsFr extends AppLocalizations { 'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Source de métadonnées par défaut'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Définir la source de métadonnées par défaut'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Source audio par défaut'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Définir la source audio par défaut'; @override String get set_default => 'Définir par défaut'; @@ -1563,7 +1564,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configurez vos propres plugins de fournisseur de métadonnées et de source audio'; @override String get audio_scrobblers => 'Scrobblers audio'; @@ -1572,12 +1573,12 @@ class AppLocalizationsFr extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Source : '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Non compressé'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Pour les audiophiles. Fournit des flux audio de haute qualité/sans perte. Correspondance précise des pistes basée sur ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 9a483e3a..0434e8db 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -872,16 +872,16 @@ class AppLocalizationsHi extends AppLocalizations { String get restore_defaults => 'डिफ़ॉल्ट सेटिंग्स को बहाल करें'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'संगीत डाउनलोड प्रारूप'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'संगीत स्ट्रीमिंग प्रारूप'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'संगीत डाउनलोड गुणवत्ता'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'संगीत स्ट्रीमिंग गुणवत्ता'; @override String get login_with_lastfm => 'Last.fm से लॉगिन करें'; @@ -1454,16 +1454,16 @@ class AppLocalizationsHi extends AppLocalizations { 'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'डिफ़ॉल्ट मेटाडेटा स्रोत'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'डिफ़ॉल्ट मेटाडेटा स्रोत सेट करें'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'डिफ़ॉल्ट ऑडियो स्रोत'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'डिफ़ॉल्ट ऑडियो स्रोत सेट करें'; @override String get set_default => 'डिफ़ॉल्ट सेट करें'; @@ -1524,7 +1524,7 @@ class AppLocalizationsHi extends AppLocalizations { 'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है'; @override - String get plugins => 'Plugins'; + String get plugins => 'प्लगइन्स'; @override String get paste_plugin_download_url => @@ -1550,7 +1550,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'अपने स्वयं के मेटाडेटा प्रदाता और ऑडियो स्रोत प्लगइन्स कॉन्फ़िगर करें'; @override String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स'; @@ -1559,12 +1559,12 @@ class AppLocalizationsHi extends AppLocalizations { String get scrobbling => 'स्क्रॉबलिंग'; @override - String get source => 'Source: '; + String get source => 'स्रोत: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'असंपीड़ित'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'ऑडियोफाइलों के लिए। उच्च-गुणवत्ता/बिना हानि वाले ऑडियो स्ट्रीम प्रदान करता है। सटीक ISRC आधारित ट्रैक मिलान।'; } diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 68a078b4..ce250425 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -874,16 +874,16 @@ class AppLocalizationsId extends AppLocalizations { String get restore_defaults => 'Kembalikan semula'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Format unduh musik'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Format streaming musik'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Kualitas unduh musik'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Kualitas streaming musik'; @override String get login_with_lastfm => 'Masuk dengan Last.fm'; @@ -1455,16 +1455,16 @@ class AppLocalizationsId extends AppLocalizations { 'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Sumber metadata default'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Atur sumber metadata default'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Sumber audio default'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Atur sumber audio default'; @override String get set_default => 'Atur sebagai bawaan'; @@ -1526,7 +1526,7 @@ class AppLocalizationsId extends AppLocalizations { 'Masukan tidak cocok dengan format yang diperlukan'; @override - String get plugins => 'Plugins'; + String get plugins => 'Plugin'; @override String get paste_plugin_download_url => @@ -1552,7 +1552,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Konfigurasi plugin penyedia metadata dan sumber audio Anda sendiri'; @override String get audio_scrobblers => 'Scrobblers Audio'; @@ -1561,12 +1561,12 @@ class AppLocalizationsId extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Sumber: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Tidak terkompresi'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Untuk audiophile. Menyediakan aliran audio berkualitas tinggi/tanpa kehilangan. Pencocokkan trek yang akurat berdasarkan ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 2385e466..f2dfa5ed 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -874,16 +874,16 @@ class AppLocalizationsIt extends AppLocalizations { String get restore_defaults => 'Ripristina default'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Formato download musica'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Formato streaming musica'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Qualità download musica'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Qualità streaming musica'; @override String get login_with_lastfm => 'Accesso a Last.fm'; @@ -1454,16 +1454,17 @@ class AppLocalizationsIt extends AppLocalizations { 'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Fonte metadati predefinita'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Imposta fonte metadati predefinita'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Fonte audio predefinita'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Imposta fonte audio predefinita'; @override String get set_default => 'Imposta come predefinito'; @@ -1525,7 +1526,7 @@ class AppLocalizationsIt extends AppLocalizations { 'L\'input non corrisponde al formato richiesto'; @override - String get plugins => 'Plugins'; + String get plugins => 'Plugin'; @override String get paste_plugin_download_url => @@ -1551,7 +1552,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configura i tuoi plugin per fornitore metadati e fonte audio'; @override String get audio_scrobblers => 'Scrobbler audio'; @@ -1560,12 +1561,12 @@ class AppLocalizationsIt extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Fonte: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Non compresso'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Per audiophile. Fornisce flussi audio di alta qualità/senza perdita. Abbinamento traccia accurato basato su ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 73c283de..2505f68a 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -861,16 +861,16 @@ class AppLocalizationsJa extends AppLocalizations { String get restore_defaults => '設定を初期化'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => '音楽ダウンロード形式'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => '音楽ストリーミング形式'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => '音楽ダウンロード品質'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => '音楽ストリーミング品質'; @override String get login_with_lastfm => 'Last.fmでログイン'; @@ -1422,16 +1422,16 @@ class AppLocalizationsJa extends AppLocalizations { String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'デフォルトメタデータソース'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'デフォルトメタデータソースを設定'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'デフォルトオーディオソース'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'デフォルトオーディオソースを設定'; @override String get set_default => 'デフォルトに設定'; @@ -1489,7 +1489,7 @@ class AppLocalizationsJa extends AppLocalizations { String get input_does_not_match_format => '入力が必須フォーマットと一致しません'; @override - String get plugins => 'Plugins'; + String get plugins => 'プラグイン'; @override String get paste_plugin_download_url => @@ -1514,8 +1514,7 @@ class AppLocalizationsJa extends AppLocalizations { String get available_plugins => '利用可能なプラグイン'; @override - String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + String get configure_plugins => '独自のメタデータプロバイダーとオーディオソースプラグインを設定'; @override String get audio_scrobblers => 'オーディオスクロッブラー'; @@ -1524,12 +1523,12 @@ class AppLocalizationsJa extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'ソース: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => '非圧縮'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'オーディオファイル向け。高品質/ロスレスオーディオストリームを提供。正確なISRCベースのトラックマッチング。'; } diff --git a/lib/l10n/generated/app_localizations_ka.dart b/lib/l10n/generated/app_localizations_ka.dart index 313aba60..c8557037 100644 --- a/lib/l10n/generated/app_localizations_ka.dart +++ b/lib/l10n/generated/app_localizations_ka.dart @@ -872,16 +872,16 @@ class AppLocalizationsKa extends AppLocalizations { String get restore_defaults => 'ნაგულისხმევი პარამეტრების აღდგენა'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'მუსიკის ჩამოტვირთვის ფორმატი'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'სტრიმინგის მუსიკის ფორმატი'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'ჩამოტვირთვის ხარისხი'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'სტრიმინგის ხარისხი'; @override String get login_with_lastfm => 'Last.fm-ით შესვლა'; @@ -1454,16 +1454,17 @@ class AppLocalizationsKa extends AppLocalizations { 'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'ნაგულისხმევი მეტამონაცემების წყარო'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'ნაგულისხმევი მეტამონაცემების წყაროს დაყენება'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'ნაგულისხმევი აუდიო წყარო'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'ნაგულისხმევი აუდიო წყაროს დაყენება'; @override String get set_default => 'ნაგულისხმევად დაყენება'; @@ -1526,7 +1527,7 @@ class AppLocalizationsKa extends AppLocalizations { 'შეყვანა არ ემთხვევა საჭირო ფორმატს'; @override - String get plugins => 'Plugins'; + String get plugins => 'პლაგინები'; @override String get paste_plugin_download_url => @@ -1552,7 +1553,7 @@ class AppLocalizationsKa extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'თქვენი საკუთარი მეტამონაცემებისა და აუდიო წყაროს პლაგინების კონფიგურაცია'; @override String get audio_scrobblers => 'აუდიო სქრობლერები'; @@ -1561,12 +1562,12 @@ class AppLocalizationsKa extends AppLocalizations { String get scrobbling => 'სქრობლინგი'; @override - String get source => 'Source: '; + String get source => 'წყარო: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'შეუკუმშავი'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'აუდიოფილებისთვის. უზრუნველყოფს მაღალი ხარისხის/უკომპრესო აუდიო სტრიმებს. ზუსტი შესაბამისობა ISRC-ის მიხედვით.'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 053bd3cf..42ea337a 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -862,16 +862,16 @@ class AppLocalizationsKo extends AppLocalizations { String get restore_defaults => '기본값으로 복원'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => '다운로드 음악 포맷'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => '스트리밍 음악 포맷'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => '다운로드 음질'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => '스트리밍 음질'; @override String get login_with_lastfm => 'Last.fm에 로그인'; @@ -1427,16 +1427,16 @@ class AppLocalizationsKo extends AppLocalizations { String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => '기본 메타데이터 소스'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => '기본 메타데이터 소스 설정'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => '기본 오디오 소스'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => '기본 오디오 소스 설정'; @override String get set_default => '기본값으로 설정'; @@ -1494,7 +1494,7 @@ class AppLocalizationsKo extends AppLocalizations { String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다'; @override - String get plugins => 'Plugins'; + String get plugins => '플러그인'; @override String get paste_plugin_download_url => @@ -1518,8 +1518,7 @@ class AppLocalizationsKo extends AppLocalizations { String get available_plugins => '사용 가능한 플러그인'; @override - String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + String get configure_plugins => '직접 메타데이터 제공자와 오디오 소스 플러그인을 구성하세요'; @override String get audio_scrobblers => '오디오 스크로블러'; @@ -1528,12 +1527,12 @@ class AppLocalizationsKo extends AppLocalizations { String get scrobbling => '스크로블링'; @override - String get source => 'Source: '; + String get source => '출처: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => '비압축'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + '오디오파일을 위한 소스입니다. 고음질/무손실 오디오 스트림을 제공하며 ISRC 기반으로 정확한 트랙 매칭을 지원합니다.'; } diff --git a/lib/l10n/generated/app_localizations_ne.dart b/lib/l10n/generated/app_localizations_ne.dart index 5fe152ee..8f881b51 100644 --- a/lib/l10n/generated/app_localizations_ne.dart +++ b/lib/l10n/generated/app_localizations_ne.dart @@ -880,16 +880,16 @@ class AppLocalizationsNe extends AppLocalizations { String get restore_defaults => 'पूर्वनिर्धारितहरू पुनः स्थापित गर्नुहोस्'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'सङ्गीत डाउनलोड ढाँचा'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'स्ट्रिमिङ सङ्गीत ढाँचा'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'डाउनलोड गुणस्तर'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'स्ट्रिमिङ गुणस्तर'; @override String get login_with_lastfm => 'लास्ट.एफ.एम सँग लगइन गर्नुहोस्'; @@ -1460,16 +1460,18 @@ class AppLocalizationsNe extends AppLocalizations { 'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'पूर्वनिर्धारित मेटाडाटा स्रोत'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'पूर्वनिर्धारित मेटाडाटा स्रोत सेट गर्नुहोस्'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'पूर्वनिर्धारित अडियो स्रोत'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => + 'पूर्वनिर्धारित अडियो स्रोत सेट गर्नुहोस्'; @override String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्'; @@ -1530,7 +1532,7 @@ class AppLocalizationsNe extends AppLocalizations { String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन'; @override - String get plugins => 'Plugins'; + String get plugins => 'प्लगइनहरू'; @override String get paste_plugin_download_url => @@ -1556,7 +1558,7 @@ class AppLocalizationsNe extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'आफ्नै मेटाडाटा प्रदायक र अडियो स्रोत प्लगइनहरू कन्फिगर गर्नुहोस्'; @override String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू'; @@ -1565,12 +1567,12 @@ class AppLocalizationsNe extends AppLocalizations { String get scrobbling => 'स्क्रब्बलिंग'; @override - String get source => 'Source: '; + String get source => 'स्रोत: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'असंक्षिप्त'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'अडियोप्रेमीहरूका लागि। उच्च गुणस्तर/लसलेस अडियो स्ट्रिमहरू उपलब्ध गराउँछ। ISRC-मा आधारित सटीक ट्र्याक मिलान।'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index c6585faf..0a73c640 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -815,7 +815,7 @@ class AppLocalizationsNl extends AppLocalizations { String get search_mode => 'Zoekmodus'; @override - String get audio_source => 'Audio Source'; + String get audio_source => 'Audiobron'; @override String get ok => 'Oké'; @@ -872,16 +872,16 @@ class AppLocalizationsNl extends AppLocalizations { String get restore_defaults => 'Standaardwaarden herstellen'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Download muziekformaat'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Streaming muziekformaat'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Downloadkwaliteit'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Streamingkwaliteit'; @override String get login_with_lastfm => 'Inloggen met Last.fm'; @@ -1452,16 +1452,16 @@ class AppLocalizationsNl extends AppLocalizations { 'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Standaard metadata-bron'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Standaard metadata-bron instellen'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Standaard audiobron'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Standaard audiobron instellen'; @override String get set_default => 'Instellen als standaard'; @@ -1524,7 +1524,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Invoer komt niet overeen met het vereiste formaat'; @override - String get plugins => 'Plugins'; + String get plugins => 'Plug-ins'; @override String get paste_plugin_download_url => @@ -1550,7 +1550,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configureer je eigen metadata- en audiobron-plug-ins'; @override String get audio_scrobblers => 'Audioscrobblers'; @@ -1559,12 +1559,12 @@ class AppLocalizationsNl extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Bron: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Ongecomprimeerd'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Voor audiofielen. Biedt hoge kwaliteit/lossless audiostreams. Nauwkeurige trackmatching op basis van ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index 88edf4c9..5e185035 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -873,16 +873,16 @@ class AppLocalizationsPl extends AppLocalizations { String get restore_defaults => 'Przywróć domyślne'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Format pobierania muzyki'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Format strumieniowania muzyki'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Jakość pobierania'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Jakość strumieniowania'; @override String get login_with_lastfm => 'Zaloguj się z Last.fm'; @@ -1455,16 +1455,16 @@ class AppLocalizationsPl extends AppLocalizations { 'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Domyślne źródło metadanych'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Ustaw domyślne źródło metadanych'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Domyślne źródło audio'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Ustaw domyślne źródło audio'; @override String get set_default => 'Ustaw jako domyślną'; @@ -1526,7 +1526,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Wprowadzony tekst nie pasuje do wymaganego formatu'; @override - String get plugins => 'Plugins'; + String get plugins => 'Wtyczki'; @override String get paste_plugin_download_url => @@ -1552,7 +1552,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Skonfiguruj własne wtyczki dostawców metadanych i źródeł audio'; @override String get audio_scrobblers => 'Scrobblery audio'; @@ -1561,12 +1561,12 @@ class AppLocalizationsPl extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Źródło: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Nieskompresowany'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Dla audiofilów. Oferuje strumienie audio wysokiej jakości/lossless. Precyzyjne dopasowanie utworów na podstawie ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 2d948030..8d2eabe7 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -873,16 +873,16 @@ class AppLocalizationsPt extends AppLocalizations { String get restore_defaults => 'Restaurar padrões'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Formato de download de música'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Formato de streaming de música'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Qualidade de download'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Qualidade de streaming'; @override String get login_with_lastfm => 'Iniciar sessão com o Last.fm'; @@ -1452,16 +1452,16 @@ class AppLocalizationsPt extends AppLocalizations { 'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Fonte padrão de metadados'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Definir fonte padrão de metadados'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Fonte de áudio padrão'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Definir fonte de áudio padrão'; @override String get set_default => 'Definir como padrão'; @@ -1549,7 +1549,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Configure seus próprios plugins de provedores de metadados e fontes de áudio'; @override String get audio_scrobblers => 'Scrobblers de áudio'; @@ -1558,12 +1558,12 @@ class AppLocalizationsPt extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Fonte: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Não comprimido'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Para audiófilos. Fornece streams de áudio de alta qualidade/sem perdas. Correspondência precisa de faixas baseada em ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 29257248..31be6a7b 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -874,16 +874,16 @@ class AppLocalizationsRu extends AppLocalizations { String get restore_defaults => 'Восстановить настройки по умолчанию'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Формат загрузки музыки'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Формат потоковой музыки'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Качество загрузки'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Качество стриминга'; @override String get login_with_lastfm => 'Войти с помощью Last.fm'; @@ -1454,16 +1454,17 @@ class AppLocalizationsRu extends AppLocalizations { 'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Источник метаданных по умолчанию'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Задать источник метаданных по умолчанию'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Источник аудио по умолчанию'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Задать источник аудио по умолчанию'; @override String get set_default => 'Установить по умолчанию'; @@ -1526,7 +1527,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Введенные данные не соответствуют требуемому формату'; @override - String get plugins => 'Plugins'; + String get plugins => 'Плагины'; @override String get paste_plugin_download_url => @@ -1552,7 +1553,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Настройте собственные плагины провайдеров метаданных и источников аудио'; @override String get audio_scrobblers => 'Аудио скробблеры'; @@ -1561,12 +1562,12 @@ class AppLocalizationsRu extends AppLocalizations { String get scrobbling => 'Скробблинг'; @override - String get source => 'Source: '; + String get source => 'Источник: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Несжатый'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Для аудиофилов. Предоставляет высококачественные/lossless аудиопотоки. Точное совпадение треков по ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_ta.dart b/lib/l10n/generated/app_localizations_ta.dart index 08bd253b..062a99dc 100644 --- a/lib/l10n/generated/app_localizations_ta.dart +++ b/lib/l10n/generated/app_localizations_ta.dart @@ -879,16 +879,16 @@ class AppLocalizationsTa extends AppLocalizations { String get restore_defaults => 'இயல்புநிலைகளை மீட்டமை'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'இசை பதிவிறக்க வடிவம்'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'இசை ஸ்ட்ரீமிங் வடிவம்'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'பதிவிறக்க தரம்'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'ஸ்ட்ரீமிங் தரம்'; @override String get login_with_lastfm => 'Last.fm உடன் உள்நுழைக'; @@ -1461,16 +1461,17 @@ class AppLocalizationsTa extends AppLocalizations { 'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'இயல்புநிலை மெட்டாடேட்டா மூலம்'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'இயல்புநிலை மெட்டாடேட்டா மூலத்தை அமை'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'இயல்புநிலை ஆடியோ மூலம்'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'இயல்புநிலை ஆடியோ மூலத்தை அமை'; @override String get set_default => 'இயல்புநிலையாக அமைக்கவும்'; @@ -1532,7 +1533,7 @@ class AppLocalizationsTa extends AppLocalizations { 'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை'; @override - String get plugins => 'Plugins'; + String get plugins => 'செருகுநிரல்கள்'; @override String get paste_plugin_download_url => @@ -1558,7 +1559,7 @@ class AppLocalizationsTa extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'உங்கள் சொந்த மெட்டாடேட்டா வழங்குநர் மற்றும் ஆடியோ மூல செருகுநிரல்களை அமைக்கவும்'; @override String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்'; @@ -1567,12 +1568,12 @@ class AppLocalizationsTa extends AppLocalizations { String get scrobbling => 'ஸ்க்ரோப்ளிங்'; @override - String get source => 'Source: '; + String get source => 'மூலம்: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'அழுத்தப்படாத'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'ஆடியோஃபைல்களுக்காக. உயர்தர/லாஸ்லெஸ் ஆடியோ ஸ்ட்ரீம்களை வழங்குகிறது. ISRC அடிப்படையில் துல்லியமான பாடல் பொருத்தம்.'; } diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 0dc52d64..16584ab8 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -872,16 +872,16 @@ class AppLocalizationsTh extends AppLocalizations { String get restore_defaults => 'คืนค่าเริ่มต้น'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'รูปแบบการดาวน์โหลดเพลง'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'รูปแบบการสตรีมเพลง'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'คุณภาพการดาวน์โหลด'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'คุณภาพการสตรีม'; @override String get login_with_lastfm => 'เข้าสู่ระบบด้วย Last.fm'; @@ -1446,16 +1446,16 @@ class AppLocalizationsTh extends AppLocalizations { 'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'แหล่งเมตาดาต้าพื้นฐาน'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'ตั้งค่าแหล่งเมตาดาต้าพื้นฐาน'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'แหล่งเสียงพื้นฐาน'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'ตั้งค่าแหล่งเสียงพื้นฐาน'; @override String get set_default => 'ตั้งค่าเริ่มต้น'; @@ -1515,7 +1515,7 @@ class AppLocalizationsTh extends AppLocalizations { String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ'; @override - String get plugins => 'Plugins'; + String get plugins => 'ปลั๊กอิน'; @override String get paste_plugin_download_url => @@ -1541,7 +1541,7 @@ class AppLocalizationsTh extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'กำหนดค่าปลั๊กอินผู้ให้บริการเมตาดาต้าและแหล่งเสียงของคุณเอง'; @override String get audio_scrobblers => 'เครื่อง scrobbler เสียง'; @@ -1550,12 +1550,12 @@ class AppLocalizationsTh extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'แหล่งที่มา: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'ไม่บีบอัด'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'สำหรับคนรักเสียงเพลง ให้สตรีมเสียงคุณภาพสูง/ไร้การสูญเสียการบีบอัด การจับคู่แทร็กแม่นยำตาม ISRC'; } diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index e35f9f04..5febc92d 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -878,16 +878,16 @@ class AppLocalizationsTl extends AppLocalizations { String get restore_defaults => 'Ibalik ang mga default'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'I-download na format ng musika'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Format ng streaming ng musika'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Kalidad ng i-download na musika'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Kalidad ng streaming ng musika'; @override String get login_with_lastfm => 'Mag-login gamit ang Last.fm'; @@ -1462,16 +1462,18 @@ class AppLocalizationsTl extends AppLocalizations { 'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Default na pinagmulan ng metadata'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Itakda ang default na pinagmulan ng metadata'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Default na pinagmulan ng audio'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => + 'Itakda ang default na pinagmulan ng audio'; @override String get set_default => 'Itakda bilang default'; @@ -1533,7 +1535,7 @@ class AppLocalizationsTl extends AppLocalizations { 'Ang input ay hindi tumutugma sa kinakailangang format'; @override - String get plugins => 'Plugins'; + String get plugins => 'Mga plugin'; @override String get paste_plugin_download_url => @@ -1559,7 +1561,7 @@ class AppLocalizationsTl extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'I-configure ang sarili mong metadata provider at mga audio source plugin'; @override String get audio_scrobblers => 'Mga Audio Scrobbler'; @@ -1568,12 +1570,12 @@ class AppLocalizationsTl extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Pinagmulan: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Hindi naka-compress'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Para sa mga audiophile. Nagbibigay ng de-kalidad/walang loss na audio streams. Tumpak na pagtutugma ng track batay sa ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 281d6ae9..c2280f47 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -875,16 +875,16 @@ class AppLocalizationsTr extends AppLocalizations { String get restore_defaults => 'Varsayılanları geri yükle'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Müzik indirme formatı'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Müzik akış formatı'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'İndirilen müzik kalitesi'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Yayınlanan müzik kalitesi'; @override String get login_with_lastfm => 'Last.fm ile giriş yap'; @@ -1456,16 +1456,17 @@ class AppLocalizationsTr extends AppLocalizations { 'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Varsayılan meta veri kaynağı'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Varsayılan meta veri kaynağını ayarla'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Varsayılan ses kaynağı'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Varsayılan ses kaynağını ayarla'; @override String get set_default => 'Varsayılan olarak ayarla'; @@ -1526,7 +1527,7 @@ class AppLocalizationsTr extends AppLocalizations { String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor'; @override - String get plugins => 'Plugins'; + String get plugins => 'Eklentiler'; @override String get paste_plugin_download_url => @@ -1552,7 +1553,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Kendi meta veri sağlayıcı ve ses kaynağı eklentilerinizi yapılandırın'; @override String get audio_scrobblers => 'Ses Scrobbler\'lar'; @@ -1561,12 +1562,12 @@ class AppLocalizationsTr extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Kaynak: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Sıkıştırılmamış'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Audiophile\'ler için. Yüksek kaliteli/kayıpsız ses akışları sağlar. Doğru ISRC tabanlı parça eşleştirme.'; } diff --git a/lib/l10n/generated/app_localizations_uk.dart b/lib/l10n/generated/app_localizations_uk.dart index 66496a17..c2bed426 100644 --- a/lib/l10n/generated/app_localizations_uk.dart +++ b/lib/l10n/generated/app_localizations_uk.dart @@ -875,16 +875,16 @@ class AppLocalizationsUk extends AppLocalizations { String get restore_defaults => 'Відновити налаштування за замовчуванням'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Формат завантаження музики'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Формат потокової музики'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Якість завантаженої музики'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Якість потокової музики'; @override String get login_with_lastfm => 'Увійти з Last.fm'; @@ -1452,16 +1452,18 @@ class AppLocalizationsUk extends AppLocalizations { 'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Джерело метаданих за замовчуванням'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => + 'Встановити джерело метаданих за замовчуванням'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Джерело аудіо за замовчуванням'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => + 'Встановити джерело аудіо за замовчуванням'; @override String get set_default => 'Встановити за замовчуванням'; @@ -1522,7 +1524,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Введені дані не відповідають необхідному формату'; @override - String get plugins => 'Plugins'; + String get plugins => 'Плагіни'; @override String get paste_plugin_download_url => @@ -1548,7 +1550,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Налаштуйте власні плагіни метаданих і аудіоджерела'; @override String get audio_scrobblers => 'Аудіо скробблери'; @@ -1557,12 +1559,12 @@ class AppLocalizationsUk extends AppLocalizations { String get scrobbling => 'Скроблінг'; @override - String get source => 'Source: '; + String get source => 'Джерело: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Без стиснення'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Для аудіофілів. Забезпечує високоякісні/без втрат аудіопотоки. Точна відповідність треків на основі ISRC.'; } diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 4a6bbafd..4d7a8945 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -875,16 +875,16 @@ class AppLocalizationsVi extends AppLocalizations { String get restore_defaults => 'Khôi phục mặc định'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => 'Định dạng nhạc tải về'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => 'Định dạng nhạc phát trực tuyến'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => 'Chất lượng nhạc tải về'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => 'Chất lượng nhạc phát trực tuyến'; @override String get login_with_lastfm => 'Đăng nhập bằng tài khoản Last.fm'; @@ -1456,16 +1456,16 @@ class AppLocalizationsVi extends AppLocalizations { 'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => 'Nguồn siêu dữ liệu mặc định'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => 'Đặt nguồn siêu dữ liệu mặc định'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => 'Nguồn âm thanh mặc định'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => 'Đặt nguồn âm thanh mặc định'; @override String get set_default => 'Đặt làm mặc định'; @@ -1528,7 +1528,7 @@ class AppLocalizationsVi extends AppLocalizations { 'Đầu vào không khớp với định dạng yêu cầu'; @override - String get plugins => 'Plugins'; + String get plugins => 'Tiện ích bổ sung'; @override String get paste_plugin_download_url => @@ -1554,7 +1554,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + 'Cấu hình nhà cung cấp siêu dữ liệu và tiện ích nguồn âm thanh riêng'; @override String get audio_scrobblers => 'Bộ scrobbler âm thanh'; @@ -1563,12 +1563,12 @@ class AppLocalizationsVi extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => 'Nguồn: '; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => 'Không nén'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + 'Dành cho người yêu âm nhạc chất lượng cao. Cung cấp luồng âm thanh chất lượng cao/không nén. Phù hợp bài hát dựa trên ISRC chính xác.'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index f1a25912..ac7d4890 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -859,16 +859,16 @@ class AppLocalizationsZh extends AppLocalizations { String get restore_defaults => '恢复默认值'; @override - String get download_music_format => 'Download music format'; + String get download_music_format => '下载音乐格式'; @override - String get streaming_music_format => 'Streaming music format'; + String get streaming_music_format => '流媒体音乐格式'; @override - String get download_music_quality => 'Download music quality'; + String get download_music_quality => '下载音乐质量'; @override - String get streaming_music_quality => 'Streaming music quality'; + String get streaming_music_quality => '流媒体音乐质量'; @override String get login_with_lastfm => '使用 Last.fm 登录'; @@ -1418,16 +1418,16 @@ class AppLocalizationsZh extends AppLocalizations { String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。'; @override - String get default_metadata_source => 'Default metadata source'; + String get default_metadata_source => '默认元数据源'; @override - String get set_default_metadata_source => 'Set default metadata source'; + String get set_default_metadata_source => '设置默认元数据源'; @override - String get default_audio_source => 'Default audio source'; + String get default_audio_source => '默认音频源'; @override - String get set_default_audio_source => 'Set default audio source'; + String get set_default_audio_source => '设置默认音频源'; @override String get set_default => '设为默认'; @@ -1484,7 +1484,7 @@ class AppLocalizationsZh extends AppLocalizations { String get input_does_not_match_format => '输入与所需格式不匹配'; @override - String get plugins => 'Plugins'; + String get plugins => '插件'; @override String get paste_plugin_download_url => @@ -1508,8 +1508,7 @@ class AppLocalizationsZh extends AppLocalizations { String get available_plugins => '可用插件'; @override - String get configure_plugins => - 'Configure your own metadata provider and audio source plugins'; + String get configure_plugins => '配置您自己的元数据提供者和音频源插件'; @override String get audio_scrobblers => '音频 Scrobblers'; @@ -1518,14 +1517,14 @@ class AppLocalizationsZh extends AppLocalizations { String get scrobbling => 'Scrobbling'; @override - String get source => 'Source: '; + String get source => '来源:'; @override - String get uncompressed => 'Uncompressed'; + String get uncompressed => '无损'; @override String get dab_music_source_description => - 'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'; + '适合发烧友。提供高质量/无损音频流。基于 ISRC 的精确曲目匹配。'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). @@ -2382,6 +2381,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get restore_defaults => '恢復預設值'; + @override + String get download_music_format => '下載音樂格式'; + + @override + String get streaming_music_format => '串流音樂格式'; + + @override + String get download_music_quality => '下載音樂品質'; + + @override + String get streaming_music_quality => '串流音樂品質'; + @override String get login_with_lastfm => '使用 Last.fm 登入'; @@ -2929,6 +2940,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。'; + @override + String get default_metadata_source => '預設中繼資料來源'; + + @override + String get set_default_metadata_source => '設定預設中繼資料來源'; + + @override + String get default_audio_source => '預設音訊來源'; + + @override + String get set_default_audio_source => '設定預設音訊來源'; + @override String get set_default => '設為預設'; @@ -2983,6 +3006,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get input_does_not_match_format => '輸入不符合所需格式'; + @override + String get plugins => '外掛程式'; + @override String get paste_plugin_download_url => '貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結'; @@ -3004,9 +3030,22 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get available_plugins => '可用的外掛程式'; + @override + String get configure_plugins => '配置您自己的中繼資料提供者和音訊來源外掛程式'; + @override String get audio_scrobblers => '音訊 Scrobblers'; @override String get scrobbling => 'Scrobbling'; + + @override + String get source => '來源:'; + + @override + String get uncompressed => '未壓縮'; + + @override + String get dab_music_source_description => + '適合音響發燒友。提供高品質/無損音訊串流。精確的 ISRC 曲目比對。'; } diff --git a/pubspec.yaml b/pubspec.yaml index a93cb456..6314b733 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source extensible music streaming platform and app, based on B publish_to: "none" -version: 5.0.0+42 +version: 5.1.0+43 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube diff --git a/untranslated_messages.json b/untranslated_messages.json index 2ca92876..9e26dfee 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,466 +1 @@ -{ - "ar": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "bn": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ca": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "cs": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "de": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "es": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "eu": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "fa": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "fi": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "fr": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "hi": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "id": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "it": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ja": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ka": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ko": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ne": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "nl": [ - "audio_source", - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "pl": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "pt": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ru": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "ta": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "th": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "tl": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "tr": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "uk": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "vi": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "zh": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ], - - "zh_TW": [ - "download_music_format", - "streaming_music_format", - "download_music_quality", - "streaming_music_quality", - "default_metadata_source", - "set_default_metadata_source", - "default_audio_source", - "set_default_audio_source", - "plugins", - "configure_plugins", - "source", - "uncompressed", - "dab_music_source_description" - ] -} +{} \ No newline at end of file From b14292841263e85c83a9a611fa75ae1cb89abcc2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 13:38:47 +0600 Subject: [PATCH 43/47] chore: remove buffer size limit --- lib/services/audio_player/audio_player.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 9ae4e973..2693f13a 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -49,7 +49,6 @@ abstract class AudioPlayerInterface { configuration: const mk.PlayerConfiguration( title: "Spotube", logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, - bufferSize: 4 * 1024 * 1024, // 4MB buffer async: true, ), ) { From 89c67e4f8950bdc1835a8ea3a2a5181c7509adaf Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 14:19:12 +0600 Subject: [PATCH 44/47] docs: update readme credits --- README.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 153832ea..1043fabc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Spotube Logo A cross-platform extensible open-source music streaming platform.
-Bring your own music metadata/playlist with plugins created by community or by yourself. A small step towards the decentralized music streaming era! +Bring your own music metadata/playlist/audio-source with plugins created by community or by yourself. A small step towards the decentralized music streaming era! Btw it's not just another Electron app 😉 @@ -202,6 +202,7 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube. 1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader. 1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites. +1. [YouTubeExplodeDart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. 1. [LRCLib](https://lrclib.net/) - A public synced lyric API. 1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution 1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users @@ -215,7 +216,6 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included). 1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. -1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library. 1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off. 1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. 1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour. @@ -242,15 +242,11 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. 1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more. 1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. -1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. 1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app. 1. [flutter_undraw](https://github.com/KRTirtho/flutter_undraw) - Undraw.co Illustrations for Flutter with customization options 1. [form_builder_validators](https://github.com/flutter-form-builder-ecosystem) - Form Builder Validators set of validators for FlutterFormBuilder. Provides common validators and a way to make your own. -1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets 1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too. 1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! -1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views. -1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling. 1. [home_widget](https://pub.dev/packages/home_widget) - A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS. 1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. 1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser. @@ -258,15 +254,10 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests. 1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. 1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues. -1. [invidious](https://pub.dev/packages/invidious) - Invidious API client for Dart and Flutter. -1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com -1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package. 1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications. 1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs. 1. [logging](https://pub.dev/packages/logging) - Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger. 1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics. -1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular. -1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms. 1. [metadata_god](https://pub.dev/packages/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. 1. [open_file](https://pub.dev/packages/open_file) - A plug-in that can call native APP to open files with string result in flutter, support iOS(UTI) / android(intent) / PC(ffi) / web(dart:html) @@ -275,7 +266,6 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. 1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. -1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video 1. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. 1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter 1. [shadcn_flutter](https://github.com/sunarya-thito/shadcn_flutter) - Beautifully designed components from Shadcn/UI is now available for Flutter @@ -290,9 +280,6 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet. 1. [sqlite3](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3) - Provides lightweight yet convenient bindings to SQLite by using dart:ffi 1. [sqlite3_flutter_libs](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs) - Flutter plugin to include native sqlite3 libraries with your app -1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget -1. [system_theme](https://github.com/bdlukaa/system_theme/tree/master/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS -1. [test](https://pub.dev/packages/test) - A full featured library for writing and running Dart tests across platforms. 1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime. 1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos. 1. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray. @@ -304,12 +291,17 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel. 1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter 1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry. -1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. +1. [window_manager](https://leanflutter.dev) - This plugin allows Flutter desktop apps to resizing and repositioning the window. 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. 1. [http_parser](https://pub.dev/packages/http_parser) - A platform-independent package for parsing and serializing HTTP formats. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. -1. [otp_util](https://github.com/dushiling) - otp_util is a dart package to generate and verify one-time passwords,it It provides two methods TOPT and HOTP.They are Time-based OTPs and Counter-based OTPs. -1. [dio_http2_adapter](https://github.com/cfug/dio) - An adapter that combines HTTP/2 and dio. Supports reusing connections, header compression, etc. +1. [archive](https://pub.dev/packages/archive) - Provides encoders and decoders for various archive and compression formats such as zip, tar, bzip2, gzip, and zlib. +1. [hetu_script](https://github.com/hetu-script/hetu-script) - Hetu is a lightweight scripting language for embedding in Flutter apps. +1. [get_it](https://github.com/flutter-it/get_it) - Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App" +1. [flutter_markdown_plus](https://pub.dev/packages/flutter_markdown_plus) - A Markdown renderer for Flutter. Create rich text output, including text styles, tables, links, and more, from plain text data formatted with simple Markdown tags. +1. [pub_semver](https://pub.dev/packages/pub_semver) - Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. +1. [change_case](https://github.com/mrgnhnt96/change_case) - An extension on String for the missing methods for camelCase, PascalCase, Capital Case, snake_case, param-case, CONSTANT_CASE and others. +1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs. @@ -320,17 +312,23 @@ If you are curious, you can [read the reason of choosing this license](https://d 1. [process_run](https://github.com/tekartik/process_run.dart/blob/master/packages/process_run) - Process run helpers for Linux/Win/Mac and which like feature for finding executables. 1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. 1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. -1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents. 1. [io](https://pub.dev/packages/io) - Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values. 1. [drift_dev](https://drift.simonbinder.eu/) - Dev-dependency for users of drift. Contains the generator and development tools. +1. [test](https://pub.dev/packages/test) - A full featured library for writing and running Dart tests across platforms. 1. [auto_route_generator](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you. 1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application. 1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc 1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item. 1. [flutter_broadcasts](https://github.com/KRTirtho/flutter_broadcasts.git) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications. 1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark. -1. [yt_dlp_dart](https://github.com/KRTirtho/yt_dlp_dart.git) - yt-dlp binding in Dart +1. [yt_dlp_dart](https://github.com/KRTirtho/yt_dlp_dart.git) - A starting point for Dart libraries or applications. 1. [flutter_new_pipe_extractor](https://github.com/KRTirtho/flutter_new_pipe_extractor) - NewPipeExtractor binding for Flutter (Android only) +1. [hetu_std](https://github.com/hetu-community/hetu_std.git) - A sample command-line application. +1. [hetu_otp_util](https://github.com/hetu-community/hetu_otp_util.git) - A sample command-line application. +1. [hetu_spotube_plugin](https://github.com/KRTirtho/hetu_spotube_plugin) - A new Flutter package project. +1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular. +1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms. +

© Copyright Spotube 2025

From 77c32a27cfd5e820a26effc9a0c5d3a1d12808d8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 14:26:46 +0600 Subject: [PATCH 45/47] chor: idkl --- pubspec.lock | 2 +- pubspec.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index f47f35cb..f5eea18c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -154,7 +154,7 @@ packages: source: hosted version: "3.0.0" base32: - dependency: transitive + dependency: "direct main" description: name: base32 sha256: "37548444aaee8bd5e91db442ce69ee3a79d3652ed47c1fa7568aa3bb9af0aea5" diff --git a/pubspec.yaml b/pubspec.yaml index 6314b733..add4a2a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -152,6 +152,7 @@ dependencies: git: url: https://github.com/media-kit/media-kit path: libs/universal/media_kit_libs_audio + base32: ^2.2.0 dev_dependencies: build_runner: ^2.4.13 From 7ad20666841238e7e77a15cf2dc67e5763d80ee8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 15:07:51 +0600 Subject: [PATCH 46/47] website: add AppImage link --- website/src/collections/app.ts | 54 +++++++++++++------ .../src/modules/downloads/download-item.astro | 2 +- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/website/src/collections/app.ts b/website/src/collections/app.ts index 22e567a9..6f021abe 100644 --- a/website/src/collections/app.ts +++ b/website/src/collections/app.ts @@ -8,6 +8,7 @@ import { FaUbuntu, FaWindows, FaRedhat, + FaLinux, } from "react-icons/fa6"; import { LuHouse, LuNewspaper, LuDownload, LuBook } from "react-icons/lu"; @@ -61,11 +62,21 @@ export const extendedDownloadLinks: Record< [FaUbuntu, FaDebian], "deb", ], - // "Fedora, Redhat, Opensuse": [ - // `${releasesUrl}/Spotube-linux-x86_64.rpm`, - // [FaFedora, FaRedhat, FaOpensuse], - // "rpm", - // ], + "Fedora, Redhat, Opensuse": [ + `${releasesUrl}/Spotube-linux-x86_64.rpm`, + [FaFedora, FaRedhat, FaOpensuse], + "rpm", + ], + "Linux AppImage (x64)": [ + `${releasesUrl}/Spotube-linux-x86_64.AppImage`, + [FaLinux], + "AppImage", + ], + "Linux AppImage (arm64)": [ + `${releasesUrl}/Spotube-linux-aarch64.AppImage`, + [FaLinux], + "AppImage", + ], iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], }; @@ -76,28 +87,39 @@ export const extendedNightlyDownloadLinks: Record< string, [string, IconType[], string] > = { - Android: [ - `${nightlyReleaseUrl}/Spotube-android-all-arch.apk`, - [FaAndroid], - "apk", - ], + Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid], "apk"], Windows: [ - `${nightlyReleaseUrl}/Spotube-windows-x86_64-setup.exe`, + `${releasesUrl}/Spotube-windows-x86_64-setup.exe`, [FaWindows], "exe", ], - macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], - "Ubuntu, Debian": [ - `${nightlyReleaseUrl}/Spotube-linux-x86_64.deb`, + macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], + "Ubuntu, Debian (x64)": [ + `${releasesUrl}/Spotube-linux-x86_64.deb`, + [FaUbuntu, FaDebian], + "deb", + ], + "Ubuntu, Debian (arm64)": [ + `${releasesUrl}/Spotube-linux-aarch64.deb`, [FaUbuntu, FaDebian], "deb", ], "Fedora, Redhat, Opensuse": [ - `${nightlyReleaseUrl}/Spotube-linux-x86_64.rpm`, + `${releasesUrl}/Spotube-linux-x86_64.rpm`, [FaFedora, FaRedhat, FaOpensuse], "rpm", ], - iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], + "Linux AppImage (x64)": [ + `${releasesUrl}/Spotube-linux-x86_64.AppImage`, + [FaLinux], + "AppImage", + ], + "Linux AppImage (arm64)": [ + `${releasesUrl}/Spotube-linux-aarch64.AppImage`, + [FaLinux], + "AppImage", + ], + iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], }; export const ADS_SLOTS = Object.freeze({ diff --git a/website/src/modules/downloads/download-item.astro b/website/src/modules/downloads/download-item.astro index 416a6b79..e5c5c04a 100644 --- a/website/src/modules/downloads/download-item.astro +++ b/website/src/modules/downloads/download-item.astro @@ -21,7 +21,7 @@ const { links } = Astro.props; const Icon = icon; return ; })} -

+

{link[1][2]}

From 4838656dcc518896792ee0c6c24e2c05bd756808 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Nov 2025 16:17:37 +0600 Subject: [PATCH 47/47] chore: fix alternative sources not persisting --- .../plugin.smplug | Bin 20876 -> 20906 bytes lib/modules/home/sections/new_releases.dart | 2 +- lib/modules/home/sections/sections.dart | 2 +- lib/pages/library/user_albums.dart | 2 +- lib/pages/library/user_artists.dart | 2 +- lib/pages/library/user_playlists.dart | 2 +- lib/pages/search/search.dart | 2 +- lib/provider/audio_player/audio_player.dart | 66 +++++++----------- lib/provider/metadata_plugin/album/album.dart | 2 +- .../metadata_plugin/artist/artist.dart | 2 +- .../audio_source/quality_presets.dart | 5 +- .../metadata_plugin/library/playlists.dart | 2 +- .../metadata_plugin/playlist/playlist.dart | 2 +- lib/provider/metadata_plugin/search/all.dart | 4 +- .../metadata_plugin/tracks/track.dart | 2 +- .../metadata_plugin/utils/common.dart | 2 +- .../track_options/track_options_provider.dart | 2 +- lib/services/metadata/errors/exceptions.dart | 12 +++- lib/services/sourced_track/sourced_track.dart | 23 ++++-- 19 files changed, 70 insertions(+), 66 deletions(-) diff --git a/assets/plugins/spotube-plugin-youtube-audio/plugin.smplug b/assets/plugins/spotube-plugin-youtube-audio/plugin.smplug index f8dedd0c268be48959b60ec91d95d73854e9a4d0..55aa2895d668dad6e69184fbf8d67f957cd3d7bd 100644 GIT binary patch delta 3548 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 T@`Ui;5JNi|a)L_@{+av>?d-ra delta 3564 zcma*qXEYlQ*8uQXF`KriO@!EcS844kA@+_fQZuN%(wY^c#Q39DQEIP-7Nu0t+FE;6 z{jHb@RipIn|9Ri@zUO^DJ@?#mKiqTgIrqc;@~bW*ODQ9}W(=dCWCj2LG=SM??~H2@ zAj*c>ZznQ;x=IVX&3$h zH2hLoJ!=RIDTWk7E$Tn3ow4Y>6wO9wd_KSg{T$x)8_co4^?E)p`f_<^=3sxhYd^!p z5_wDTcLJ&sg*F%&{*$C_l}AeTWA@!thwtvI9BR8N=@$rlP+zm#RroIbV6q~YZYgkq zSP6BCCLw>1Suf`qe@*&hYms0h z-0_@;+f8~t_j6}o1?5vU{1>P6LB;uB=Ua<}W$ zVq%<*kDCgwtH&tWo|b;nAmM^>QAkcKvIK%3YuI*3Y1-ux0>x7KhIu7h3w(MJ{iZh! z3pGpnW|u@&Ejg^8dC>dQbYxx<^dnRzQ#-PDf)Xv6VckS7M>wGLc!8dJF{O{Q^PZR1 zVrczDqh@8--us>BKPjgw={Qi7+&`mp;0ya_8skIrPXZ`;+!4jGrXI|Z0CZ?-152(5w>+_vR3+z zE>nt2{;X`=WQJMqDyuBZiGD}Lm3orqRI_Oms{My*^EzlcAs;Srl& zwOI%#y&X2iGKq-K%EDB32(TnoWukW{V6+E{{`92+RvcdC>{J&rIIY&j3esAQ5k=9H z(`1yb6&;!=iwWygFb zCYd=dVq+>wxA9cuL#dc3<=%5<`Qi_WbcyU=bjtV}HvHwCFt^m_!8*k$f2U6#`E5*o zucHd0C!zA#dmPyhdLKQZedtML$E$0b8grLgveNR+`}2o0ElR#{BQu5Vyu5dH%k2jb zjeq{wA$&}Clua_7ma9+wcEKmN1-@5Cz{iYj-G;iw@e7%(>CUB3Bs(Mh*pt8Elk+w# zcByZ_??^#1oMX(_$DMGMJ>k$yjp-Wj>59;1#2l(WQL7eQ5KdR2;Qr?|$X3;O)jk4c z6)SVk<^!(l!jqNAqGO+6|!^MMb{LrAXWw;EFYs+T8`ss_Fdwi3kVpmy73@0+@ zb-x+I)CRNCnikQG&z?bgs8lh@LIZ&oMy~5d4B}+hvH|_h>g&=aJJ6Bbfrd!p??zd3 zOkUs_W(-Nq4x0{jW0G{utpUpBBn^LG7%e({>upN-<=<d+ zf2X3tAtLQq$gGjvUufg2kI8L%JHdg9LBSn7mATH48SGFbt%6j)f3KM7K~V7#o(k_d zY{A_Hvv)|C&B(ZOXZq}nqr+`{KwLHPyEozjqv@CkLpc8>`g4^rSH|G^_8I%p zLmH!@8&~%I=K(yuOy2p0R2%CtS21(!^u_Mt=jygdD+6~IVp1l{?{_v;?7P7)e+yom z4jS|yi7xhWy%ZL19&u|OZ8l(Fnk61O11(Of7%rmeeC}Oc4qwlf{Y>?R@*9J){b;DZ z97eI)l=_FMV%D8p7t~z$v+{1>R%0@cNQ&~{l`N$Mj}*zm@7KqGCTi3Ayt051UKoPX z9dF&N!cX|imLOvQbWJTR8~&a7bQ4;Qt-?s`0M&KU0!g35-Tc%=t5s*Bg5R~eLG-`U zX;K$*Jl@1k%##h4UaLX{5J}xs#)_^y?bJH6rW0kH{q%-K z3or_#Glh38CCF*7ya{*=I&v5@zs+Y6T8EfPnQvqt3uj?-nkn*MSJcs}90M!|I9d z8E|z`Voj^UhTizZ@U+*?OS1_e@}!Wt3NiM@W7SOB4*@A;&i!G#M`R&BrF5^Yg-Dh< z;Z$f>LT<3xq`Z&SdUJ;G!8Tw*UY~MCdKN9fDZ%3BzOTRMrF6}1zASS1Y$7Ci{W-+tl2nQ zm}T4x!%X@bwv(>P?`qSH$C96UBy{qI9OFG2HvtWVeKfgP?fTLf_%U%eZh1fz=h)tg zEDH614fj+0`s0DeCDMbHdums}+U3f^;|l4Vt=9EL^l_JH=sRF9;AABNl^$hek)dsa z**$=KSU^O`obd&yu3)g_&(s2Ox*nNo)=`K}7+R|@p;4iit6S^3(mc?QczFQiNXUQQL5R5zGYlIf%t6E4WAiLc-cJz&D>`N$gRgCv2HXuKKYsXO zXg6|(CkP;XWv_yVA>^u84`cec@00zwgS7{o2y>C$YVsD`@DMLgv3s%uytsXDWa?E8 z7u2kk(--k+bc~xd)Kgzgkmjsv)-g!aU;N}zAR-(S9RnLBK)Xbi6MUVL4HvLDR}pYh z8()5&FPE;g2B&t!r@9Az52YE4{H}*CV7;r$c;sPq$RX(4_wOiZ z(!#abg*V7W2#*iwn^b5mJLGqu3NG~#7MIv;zTS8)%`0*U-^l$d^>I1`Qjd9P@Mgr> z+-r=6m8QnB(h(jos?J*2yzsQZ9otU7fiLF<xc?b z*;#fwPK?r|AZ(!t2fm;GYMa^kY$v~~)!e^c^1{1bO1dQnzT$(sbiKK!EManN*BNH) zX(+`q$of{-Pz0gKLK`MMoS&B=tE-`N&CAEWL3F`pDvH3A6MSF*n#d~`gwT!JU@XNi z%m;GzM3y4A#|O@gsiVla7tU%SI`NWkCoUu4 z>anhV;%#~1$~dmu#&x6G{4!{oP`X)XqZXJ02iYWaGj5$kYU140K)&Jr@PZ!w zn9QlWfh%xvCmWS}!%w5(L(1bAw0A)?RfEJKtQ8e8KG8;ToNw>6BrG$x>Z`%M z-#hwYNk>yYDLGu{k;)hdd={RjMk3oWq_&jO{|%{CA~>2wGWuzd zePL`|qhaTMn|ulQ9d{>gWoGF#Y_9y>1b=(RvTkOhZem1xso|V>*N7HBYg<#CBwJyd zSk((X#`YFokhx&gI)Z!0P*QEG^rp(vIQsVzK|4l{4Ffwu;ivPvd z|Jf?@x(U?eKUf%b_7jF`0suXF|9Fk { }), audioPlayer.playlistStream.listen((playlist) async { try { - // Playlist and state has to be in sync. This is only meant for - // the shuffle/re-ordering indices to be in sync - if (playlist.medias.length != state.tracks.length) { - AppLogger.log.w( - "Playlist length does not match state tracks length. Ignoring... " - "Playlist length: ${playlist.medias.length}, " - "State tracks length: ${state.tracks.length}", - ); - return; - } - - final trackGroupedById = groupBy( - state.tracks, - (query) => query.id, - ); - - final tracks = []; - - for (final media in playlist.medias) { - final track = trackGroupedById[SpotubeMedia.media(media).track.id] - ?.firstOrNull; - if (track != null) { - tracks.add(track); - } - } - - if (tracks.length != state.tracks.length) { - AppLogger.log.w("Mismatch in tracks after reordering/shuffling."); - final missingTracks = - state.tracks.where((track) => !tracks.contains(track)).toList(); - AppLogger.log.w( - "Missing tracks: ${missingTracks.map((e) => e.id).join(", ")}", - ); - } + final tracks = + playlist.medias.map((e) => SpotubeMedia.media(e).track).toList(); state = state.copyWith( tracks: tracks, @@ -434,13 +402,31 @@ class AudioPlayerNotifier extends Notifier { return; } - final currentIndex = state.currentIndex; - final currentTrack = state.activeTrack as SpotubeFullTrackObject; - final swappedMedia = SpotubeMedia(currentTrack); + final oldState = state; + await audioPlayer.stop(); - await audioPlayer.addTrackAt(swappedMedia, currentIndex + 1); - await audioPlayer.skipToNext(); - await audioPlayer.removeTrack(currentIndex); + await load( + oldState.tracks, + initialIndex: oldState.currentIndex, + autoPlay: true, + ); + state = state.copyWith( + collections: oldState.collections, + loopMode: oldState.loopMode, + playing: oldState.playing, + shuffled: false, + ); + await audioPlayer.setLoopMode(oldState.loopMode); + await _updatePlayerState( + AudioPlayerStateTableCompanion( + tracks: Value(state.tracks), + currentIndex: Value(state.currentIndex), + collections: Value(state.collections), + loopMode: Value(state.loopMode), + playing: Value(state.playing), + shuffled: Value(state.shuffled), + ), + ); } Future jumpToTrack(SpotubeTrackObject track) async { diff --git a/lib/provider/metadata_plugin/album/album.dart b/lib/provider/metadata_plugin/album/album.dart index 3a386236..394f6eb0 100644 --- a/lib/provider/metadata_plugin/album/album.dart +++ b/lib/provider/metadata_plugin/album/album.dart @@ -12,7 +12,7 @@ final metadataPluginAlbumProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin.album.getAlbum(id); diff --git a/lib/provider/metadata_plugin/artist/artist.dart b/lib/provider/metadata_plugin/artist/artist.dart index f1691657..e66309d4 100644 --- a/lib/provider/metadata_plugin/artist/artist.dart +++ b/lib/provider/metadata_plugin/artist/artist.dart @@ -12,7 +12,7 @@ final metadataPluginArtistProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin.artist.getArtist(artistId); diff --git a/lib/provider/metadata_plugin/audio_source/quality_presets.dart b/lib/provider/metadata_plugin/audio_source/quality_presets.dart index 0a8b00fe..ba88fed6 100644 --- a/lib/provider/metadata_plugin/audio_source/quality_presets.dart +++ b/lib/provider/metadata_plugin/audio_source/quality_presets.dart @@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/metadata/errors/exceptions.dart'; import 'package:spotube/services/metadata/metadata.dart'; part 'quality_presets.g.dart'; @@ -61,7 +62,7 @@ class AudioSourceAvailableQualityPresetsNotifier audioSourceConfigSnapshot.whenData((audioSourceConfig) { audioSourceSnapshot.whenData((audioSource) async { if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } final preferences = await SharedPreferences.getInstance(); final persistedStateStr = @@ -114,7 +115,7 @@ class AudioSourceAvailableQualityPresetsNotifier final audioSourceConfig = await ref.read(metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig)); if (audioSourceConfig == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } final preferences = await SharedPreferences.getInstance(); diff --git a/lib/provider/metadata_plugin/library/playlists.dart b/lib/provider/metadata_plugin/library/playlists.dart index 6350d610..5793eb57 100644 --- a/lib/provider/metadata_plugin/library/playlists.dart +++ b/lib/provider/metadata_plugin/library/playlists.dart @@ -131,7 +131,7 @@ final metadataPluginIsSavedPlaylistProvider = final plugin = await ref.watch(metadataPluginProvider.future); if (plugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } final savedPlaylists = diff --git a/lib/provider/metadata_plugin/playlist/playlist.dart b/lib/provider/metadata_plugin/playlist/playlist.dart index 71062b95..9a41340d 100644 --- a/lib/provider/metadata_plugin/playlist/playlist.dart +++ b/lib/provider/metadata_plugin/playlist/playlist.dart @@ -13,7 +13,7 @@ class MetadataPluginPlaylistNotifier final metadataPlugin = await ref.read(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin; diff --git a/lib/provider/metadata_plugin/search/all.dart b/lib/provider/metadata_plugin/search/all.dart index b40ee78a..4b051e58 100644 --- a/lib/provider/metadata_plugin/search/all.dart +++ b/lib/provider/metadata_plugin/search/all.dart @@ -9,7 +9,7 @@ final metadataPluginSearchAllProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin.search.all(query); @@ -20,7 +20,7 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async { final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin.search.chips; }); diff --git a/lib/provider/metadata_plugin/tracks/track.dart b/lib/provider/metadata_plugin/tracks/track.dart index 502780e1..1beac43a 100644 --- a/lib/provider/metadata_plugin/tracks/track.dart +++ b/lib/provider/metadata_plugin/tracks/track.dart @@ -8,7 +8,7 @@ final metadataPluginTrackProvider = final metadataPlugin = await ref.watch(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return metadataPlugin.track.getTrack(trackId); diff --git a/lib/provider/metadata_plugin/utils/common.dart b/lib/provider/metadata_plugin/utils/common.dart index 087b8a1b..dc56e494 100644 --- a/lib/provider/metadata_plugin/utils/common.dart +++ b/lib/provider/metadata_plugin/utils/common.dart @@ -20,7 +20,7 @@ mixin MetadataPluginMixin final plugin = await ref.read(metadataPluginProvider.future); if (plugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } return plugin; diff --git a/lib/provider/track_options/track_options_provider.dart b/lib/provider/track_options/track_options_provider.dart index d31aba73..5aebf39c 100644 --- a/lib/provider/track_options/track_options_provider.dart +++ b/lib/provider/track_options/track_options_provider.dart @@ -95,7 +95,7 @@ class TrackOptionsActions { final metadataPlugin = await ref.read(metadataPluginProvider.future); if (metadataPlugin == null) { - throw MetadataPluginException.noDefaultPlugin(); + throw MetadataPluginException.noDefaultMetadataPlugin(); } final tracks = await metadataPlugin.track.radio(track.id); diff --git a/lib/services/metadata/errors/exceptions.dart b/lib/services/metadata/errors/exceptions.dart index 62cc3779..5bb5ac57 100644 --- a/lib/services/metadata/errors/exceptions.dart +++ b/lib/services/metadata/errors/exceptions.dart @@ -9,7 +9,8 @@ enum MetadataPluginErrorCode { pluginDownloadFailed, duplicatePlugin, pluginByteCodeFileNotFound, - noDefaultPlugin, + noDefaultMetadataPlugin, + noDefaultAudiSourcePlugin, } class MetadataPluginException implements Exception { @@ -68,10 +69,15 @@ class MetadataPluginException implements Exception { 'Plugin byte code file, plugin.out not found. Please ensure the plugin is correctly packaged.', errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound, ); - MetadataPluginException.noDefaultPlugin() + MetadataPluginException.noDefaultMetadataPlugin() : this._( 'No default metadata plugin is set. Please set a default plugin in the settings.', - errorCode: MetadataPluginErrorCode.noDefaultPlugin, + errorCode: MetadataPluginErrorCode.noDefaultMetadataPlugin, + ); + MetadataPluginException.noDefaultAudioSourcePlugin() + : this._( + 'No default audio source plugin is set. Please set a default plugin in the settings.', + errorCode: MetadataPluginErrorCode.noDefaultAudiSourcePlugin, ); @override diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 5da54fc8..385e5be6 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -12,6 +12,7 @@ import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.da import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/services/metadata/errors/exceptions.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -41,7 +42,7 @@ class SourcedTrack extends BasicSourcedTrack { final audioSourceConfig = await ref.read(metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig)); if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } final database = ref.read(databaseProvider); @@ -157,7 +158,7 @@ class SourcedTrack extends BasicSourcedTrack { final audioSource = await ref.read(audioSourcePluginProvider.future); if (audioSource == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } final videoResults = []; @@ -190,7 +191,8 @@ class SourcedTrack extends BasicSourcedTrack { } Future swapWithSibling( - SpotubeAudioSourceMatchObject sibling) async { + SpotubeAudioSourceMatchObject sibling, + ) async { if (sibling.id == info.id) { return null; } @@ -199,7 +201,7 @@ class SourcedTrack extends BasicSourcedTrack { final audioSourceConfig = await ref.read(metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig)); if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } // a sibling source that was fetched from the search results @@ -216,10 +218,19 @@ class SourcedTrack extends BasicSourcedTrack { final database = ref.read(databaseProvider); + // Delete the old Entry + await (database.sourceMatchTable.delete() + ..where( + (table) => + table.trackId.equals(query.id) & + table.sourceType.equals(audioSourceConfig.slug), + )) + .go(); + await database.into(database.sourceMatchTable).insert( SourceMatchTableCompanion.insert( trackId: query.id, - sourceInfo: Value(jsonEncode(siblings.first)), + sourceInfo: Value(jsonEncode(sibling)), sourceType: audioSourceConfig.slug, createdAt: Value(DateTime.now()), ), @@ -245,7 +256,7 @@ class SourcedTrack extends BasicSourcedTrack { final audioSourceConfig = await ref.read(metadataPluginsProvider .selectAsync((data) => data.defaultAudioSourcePluginConfig)); if (audioSource == null || audioSourceConfig == null) { - throw Exception("Dude wat?"); + throw MetadataPluginException.noDefaultAudioSourcePlugin(); } List validStreams = [];