From 74e8639435fc6e44c8e1b6ef763984f03c040033 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 4 Apr 2021 20:27:37 +0200 Subject: [PATCH 01/22] update kotlin and android gradle plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d3740b8..0cbb05a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.31" + ext.kotlin_version = "1.4.32" repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 86dfd69b4bd5c8163a6173aa48063e623901b6ee Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 17 Apr 2021 20:59:37 +0200 Subject: [PATCH 02/22] add subscription info to settings fragment * update androidx.navigation: 2.3.4 -> 2.3.5 --- app/build.gradle | 4 +- .../java/org/mosad/teapod/parser/AoDParser.kt | 22 +++++++++ .../main/fragments/AccountFragment.kt | 17 +++++++ .../drawable/ic_baseline_access_time_24.xml | 6 +++ app/src/main/res/layout/fragment_account.xml | 46 ++++++++++++++++++- app/src/main/res/values-de-rDE/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_access_time_24.xml diff --git a/app/build.gradle b/app/build.gradle index 3a624d1..af99931 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,8 +46,8 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.legacy:legacy-support-v4:1.0.0' diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index faaded4..1963c45 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -40,6 +40,7 @@ object AoDParser { private const val baseUrl = "https://www.anime-on-demand.de" private const val loginPath = "/users/sign_in" private const val libraryPath = "/animes" + private const val subscriptionPath = "/mypools" private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0" @@ -117,6 +118,25 @@ object AoDParser { return media } + /** + * get subscription info from aod website, remove "Anime-Abo" Prefix and trim + */ + fun getSubscriptionInfoAsync(): Deferred { + return GlobalScope.async(Dispatchers.IO) { + // get the subscription page + val res = Jsoup.connect(baseUrl + subscriptionPath) + .cookies(sessionCookies) + .get() + + return@async res.select("a:contains(Anime-Abo)").text() + .removePrefix("Anime-Abo").trim() + } + } + + fun getSubscriptionUrl(): String { + return baseUrl + subscriptionPath + } + fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { val episode = getMediaById(mediaId).getEpisodeById(episodeId) episode.watched = true @@ -261,6 +281,8 @@ object AoDParser { } /** + * TODO rework the media loading process, don't modify media object + * TODO catch SocketTimeoutException from loading to show a waring dialog * load streams for the media path, movies have one episode * @param media is used as call ba reference */ diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 61ca637..84a9044 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -1,5 +1,7 @@ package org.mosad.teapod.ui.activity.main.fragments +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -8,6 +10,8 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.mosad.teapod.BuildConfig import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.R @@ -31,6 +35,15 @@ class AccountFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // load subscription (async) info before anything else + binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) + GlobalScope.launch { + binding.textAccountSubscription.text = getString( + R.string.account_subscription, + AoDParser.getSubscriptionInfoAsync().await() + ) + } + binding.textAccountLogin.text = EncryptedPreferences.login binding.textInfoAboutDesc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time)) binding.textThemeSelected.text = when (Preferences.theme) { @@ -49,6 +62,10 @@ class AccountFragment : Fragment() { showLoginDialog(true) } + binding.linearAccountSubscription.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl()))) + } + binding.linearTheme.setOnClickListener { showThemeDialog() } diff --git a/app/src/main/res/drawable/ic_baseline_access_time_24.xml b/app/src/main/res/drawable/ic_baseline_access_time_24.xml new file mode 100644 index 0000000..04584e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_access_time_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 20ed76c..ec914a6 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -79,8 +79,52 @@ android:text="@string/account_login_desc" android:textColor="?textSecondary" /> - + + + + + + + + + + + + + Account Zum bearbeiten tippen + Abo %1$s + Zum verlängern tippen Info Version %1$s (%2$s) Einstellungen @@ -77,6 +79,7 @@ speichern @android:string/cancel + lädt… Anmelden fehlgeschlagen Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe2da5e..e97bfc8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,6 +44,8 @@ Account user@example.com Tap to edit + Subscription %1$s + Tap to extend Info Teapod by @Seil0 Version %1$s (%2$s) @@ -97,6 +99,7 @@ save @android:string/cancel + loading… Login failed Looks like the server is taking to long to respond. Please try again later. From 8160641b8f27b4309a39f0665420b973ed326fdd Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 17 Apr 2021 21:38:03 +0200 Subject: [PATCH 03/22] update exoplayer and gradle wrapper * exoplayer 2.13.2 -> 2.13.3 * gradle 6.7.1 -> 7.0 * remove jcenter repository (fixes #29) --- app/build.gradle | 8 ++++---- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- gradlew.bat | 21 +++------------------ 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index af99931..65100d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,10 +53,10 @@ dependencies { implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.google.android.exoplayer:exoplayer-core:2.13.2' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.2' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.2' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.2' + implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/build.gradle b/build.gradle index 0cbb05a..734f524 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 6656 zcmY+Ibx_pN*Z*PZ4(U#j1qtbvrOTyO8fghZ8kYJfEe%U|$dV!@ASKczEZq$fg48M@ z;LnHO_j#Uq?%bL4dY^md%$$4Y+&@nKC|1uHR&59YNhubGh72|a#ylPdh9V+akp|I; zPk^W-a00GrFMkz_NSADdv2G2-i6rb=cB_@WnG(**4ZO$=96R=t|NZ@|0_z&q3GwO^ ziUFcuj$a9QaZ3j?xt`5#q`sT-ufrtBP0nt3IA&dr*+VCsBzBVW?vZ6eZr0oD%t33z zm~-5IVsjy(F>;S~Pm@bxX85>Z*@(QL6i3JQc?1ryQFcC@X^2^mZWhFv|v? z49>l|nA&XNQ6#OvccUTyBMB*WO#NA;FW5|eE_K6dtVYP2G?uUZ09!`Iq1IF2gA(aS zLu@G^cQJmh=x?-YsYa@E6QnE5+1@ds&0f#OQRDl^GnIT_m84G5XY%W z;Ck6bk^Oeu*Ma-XmxI5GjqzWNbJMsQF4)WfMZEA{oxW0E32e)*JfG}3otPishIQBw zkBe6N#4pKPN>q1R6G1@5&(u#5yPEToMBB6_oEK|q z@(i5j!?;NNCv~=HvW%zF&1yWBq(nJa_#``G&SRmQvE|jePUPs{J!$TacM|e}Fsceb zx+76|mDp6@w>)^DIl{8?)6XYNRU|2plG8Jy&7(^9SdOWNKKJK&>0!z6XiN4J*Jkao z=E1y5x-XDC==Ub+8fLb#OW&{2ww{h^xlJFYAMOUd)}Xg@j?ak{7Kno6?9S~F?|6Df zHo|ijXX~`Sp;Vf!nR;m%vUhq>zvlRXsL0u*Tt?F#yR}3tF0#of{(UjitqST|!{aBA zicWh+URU}Jnc*sg9iMkf0pggpd?3TI*C-q$2QOdCC7rV+CHBmjS3O%a3VeZ$ZSs5ubJuJp%e%$LHgrj0niYjX;4kt z&2~j%@q3MO)-QGCA{>o%eZu){ou^MgC6~Z8Y=tc!qF=|TOlG3wJXbaLYr-;$Ch=2J z_UcE59Xzq&h0LsjLrcZrQSa}#=0~Lk|4?e4M z6d;v->NCC1oMti)RRc`Ys0?JXQjsZ@VdCy%Z)TptCrI>0Tte$pR!@yJesoU2dtyuW z7iFsE8)CkbiJP+OP28;(%?!9WddQZcAid@R@`*e%3W65$g9ee`zvwb(VPO+uVBq6p z{QDR%CR(2z@?&9Obm3xPi2lzvfip`7q`_7UDD|lRS}4=bsl3xQIOi0@GSvMuDQX}* z4B^(DI<${qUhcLqO`itJU;e<%%iS+R3I^_xIV1O%sp*x~;-dn` zt$8>RnSUh#rU3{-47067W^WNwTdq-t$-U>Hj%r!GD!gLa;kV zW5g6pCqV+!q8LgrI49(}fIc5K_`FLV4_E#XZ6{<>w8wzc%V9k!!Byg5-0WY+J?1*z%9~Aj4WQr1Jsn2(G!U8fFpi(wsy@JLg^d+IB0kl89 z0@Ssqf!L9JjYKK$J=978+NO*5^C)GPH2a%4hm$HROjM|N3g9ch9kDLh*nlwqy{mVM z`P(l#>3NnK%#O8tSb(VmZrG+`dRD#=Cc1P%(y5S?*Hj5E{vg&Eiw!YV>S#7_WRDVoFxT5m=gFi4)}y5V%KT8!xbsH_rmR& zsmM?%J}K$1l8d?2+m(}2c}-G`x>CY%Y&QBJRC$sKM}zN<9{IlF@yJEG<^0={$+`Hc zDodJ)gCADJ_bD#am(c2ojXKb|j+ENJ#58PAA&pZXufrFzBwnuuo+khfMgd!DMlU#v z9|JelQO~E2;d^w!RZJbt%IANIudpKSP)cssoWhq)>({nvcfCr0=9=FAIMuZm8Eo=} z|DND}8_PB5HqG(QwDvaM@orYBZ9kCkHV*rxKTy>q7n~0emErUwLbhq;VN<2nKT&*a2Ajz z;lKBzU2i8KLV`d)Y&ae)!HcGk$dO}Or%8KF@kE@jU1h@zwpw{6p4ME|uC$Za-ERR2 ztQvL&uOZLe(k{w_+J^ng+l}~N8MP>F1Z$fLu}D-WWaeu#XduP@#8JpmH(X>rIL)k3 zyXNyTIB1(IH%S&pQ{rWaTVfB$~-;RnlY z^(y7mR>@=brI>!TrA)BQsQ={b*6$=1Eqbuu6IdhJ&$YD$08AwtNr9*J?%-WT<;O1< zPl1<@yeqfZ>@s4azqTf<=I4(kU^+^Qkstm%WM-0_VLm({jFc8`5Df2Q1Y9zMZu0^! zsO_yh2Sz9K>Jq6fkYbBZocEJ6C!SdEzYDkiEtNJs{?!tA#e|oiN+VaaAobwKef_kUup&4scD?1+}Q8)DaekkMYn-FOS{J%NY za^mmJ^n`t*1p@hF*gl#L+5wr40*(ub4J#L|@oCl~@|4UvCjHBYDQv&S zhyGMAkRO^tF_dyi&XM)4mQ;k>kj?RgRo@-?==oD+ns*>bf@&fPXF|4U0&ib2 zo~1ZdmCPWf!W9#sGP@9X$;Rc`tjbz^&JY}z{}j9bl?;VC{x)TfQH$D^WowKL&4Zx@ zdSn+QV7H(e0xRfN6aBfH)Q=@weoD?dvu6^ZS)zqb>GwMmIuS8zJfaMUQx9>%k~w34 z3}_B2Jj~u=SnJ~vZPj*)UoDi_FtT=UAb#J^b4B%R6z3H%cj-1OCjU5F$ky>By1zsg z>2A0ccp29(Y<;my|J_g-r{1I@+*O$>!R3`_sFNP4e}LD1e1mM&SA`;;TR0I`_hESV zh4U*9ecK$0=lYk`{SR_cm$}iS*?yQR(}T-5ub?Wn^#RTe*^1~ya%`!xWq-F*WH@%nnZTNREA z3eUX2uM9b_w!Zo$nVTotEtzuL(88N)H~v_G=89|(@IFz~Wq6ME);z(!2^PkR2B&kE zxR)xV8PE|Hszyjp#jNf=ZIQ7JR~4Ls#Vd@mPF(7R5VO$akUq8JM+sn>ZVg(lJZ)5qjqdw(*7tuwjY#0tx+|!sTz9yV~%HOdrb#!5w9>*0LrCS z%wF$Yc6~hqVQZzoC^D<(-h0aOtk}kn<<*xF61HQr<5}efY{zXXA+PaJG7vT&{Oz(@Uu!V#Fp9%Ht!~@;6AcD z$lvlPu&yd(YnAHfpN51*)JN0aYw9gGk{NE7!Oqu4rBp}F30669;{zcH-a7w9KSpDQPIE_f9T zit? zJSjTKWbe{f{9BmSDAFO1(K0oqB4578tU0(oRBE^28X>xDA!1C&VJEiYak4_ZTM*7M`hv_ zw3;2ndv3X$zT!wa7TrId{gNE`Vxf}j5wsyX+;Kn<^$EJT`NzznjyYx=pYMkZjizEU zb;Gg8Pl_pqxg)9P)C)Hxh_-mQ;u-I_Ol>d^>q08zFF!>Z3j1-HmuME_TGZ*Ev;O0O z%e(edJfV<6t3&FKwtInnj9EeQhq9;o5oLJoiKwWF5bP2~Feh#P4oN()JT0pdq!9x* ze3D-1%AV#{G=Op$6q?*Z>s{qFn}cl@9#m@DK_Bs@fdwSN`Qe18_WnveRB583mdMG- z?<3pJC!YljOnO8=M=|Cg)jw;4>4sna`uI>Kh&F20jNOk9HX&}Ry|mHJ+?emHnbYLJ zwfkx@slh31+3nq-9G5FVDQBHWWY}&hJ-fpDf!lQdmw8dlTt#=)20X74S>c&kR(?PT zBg)Y%)q&|hW1K;`nJPAGF*c3{3`FvrhD9=Ld{3M*K&5$jRhXNsq$0CLXINax1AmXX ziF39vkNtcK6i^+G^AEY!WalGazOQ$_#tx?BQ{YY$&V&42sICVl8@AI6yv;sGnT;@f zL=}rZcJqNwrEEA=GDdEe8Z=f9>^?($oS8xGdFf1eUWTYtZF<3tu2V%noPBnd=thZ+ zO&xoc?jvXG7Xt!RTw#5VN50UjgqSntw9Y35*~pxz=8OzkXg{@S2J%+{l3Q>B_qbnl z20Deb7JM&ZSp`%X>xWpb>FF8q7Nq&4#a1}A-(-!aMDmVbz05D!NpUzVe{~72h%cOh zwQFNai2a$K|hFgDk(oPF_tuf{BV!=m0*xqSzGAJ(~XUh8rk#{YOg0ReK>4eJl z;-~u5v$}DM)#vER>F)-}y(X6rGkp<{AkiPM7rFgAV^)FUX8XmCKKaWlS4;MSEagj$ z#pvH`vLX1q{&eOm>htnk4hmv=_)ao!MCp}9ql5yfre&Py!~hBAGNBa}PH&J8K=~<% z&?!J-QaH|0bq_uo6rt*r-M>d7jm1cbW^T>s)S?L{n8v`^?VIPA+qi^6e@cM|5boqEO!p1e|_{7U3Yl6K?0xMN1bbjf0@$TE-T))w> zFe?E?g$PUT-)AJ(PS^By^D^Ed!K5iv$*_eW~VA(I3~UMy*ZcgVu0$XZC*_0PgDmUL)qTCn927LD~p$yXR_GCJ&iQ; z4*`%l-dC5pALH!y*nmhdHRh02QjW1vZL4ySucz*w3f|#`=u@@YvMV1?i!&DIa2+S< z8z!gvN3FV4I;%fl;ruFeV{jKjI~?GlgkmGBuJ<7vY|l3xMOc?S@Q#C(zo*m&JLrjT2rU9PYOniB8O~yO5<1CCcQz# z17B2m1Z{R!Y)UO#CU-Y&mOlv4*Gz%rC_YkRcO)jTUEWHDvv!GWmEihE>OKPx1J?Av z8J{-#7NsT>>R#*7**=QL)1@IR77G9JGZZiVt!=jD+i(oRV;I`JkiTSZkAXuHm-VG1 z+2-LD!!2dNEk@1@Rp|C$MD9mH^)H*G*wI(i*Rc6Vvdik+BDycYQ*=0JA3dxxha|Zg zCIW1Ye-DdpMGTEwbA^6hVC<(@0FL4dkDOYcxxC5c%MJQ^)zpA%>>~Q|Y=@)XW!px; z_Fx+xOo7>sz4QX|Ef~igE+uFnzFWP<-#||*V0`0p7E*+n5+awuOWmvR{-M*chIXgo zYiZvQMond#{F8+4Zh_;>MsaZUuhp=onH@P!7W>sq|CWv|u}Wg0vo&f4UtmLzhCwwu zJaR=IO;sQxS}h(K>9VZjnED+>9rGgB3ks+AwTy_EYH{oc)mo`451n&YH%A1@WC{;1 z=fB6n zIYp46_&u`COM&Di?$P}pPAlAF*Ss<)2Xc?=@_2|EMO?(A1u!Vc=-%bDAP#zDiYQvJ z0}+}3GaLxsMIlh6?f=iRs0K=RyvMOcWl*xqe-IBLv?K{S^hP)@K|$I+h_)pdD9r~! zxhw2u66+F(E`&6hY}B_qe>wil|#*0R0B;<@E?L zVrhXKfwRg0l8r>LuNs1QqW&39ME0sOXe8zycivGVqUOjEWpU)h|9fwp@d(8=M-WxY zeazSz6x5e`k821fgylLIbdqx~Kdh^Oj`Q!4vc*Km)^Tr-qRxPHozdvvU^#xNsKVr6aw8={70&S4y*5xeoF@Q^y596*09`XF56-N z1=Rm5?-An178o?$ix}y7gizQ9gEmGHF5AW+92DYaOcwEHnjAr~!vI>CK%h`E_tO8L Yte!%o?r4GTrVtxD61Ym!|5fq-1K$0e!T1w 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d9ca16..f371643 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c51..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index a9f778a..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell From be591a961af6bb3ca8ffe84a42e750fed8e5c4ff Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 9 May 2021 19:32:31 +0200 Subject: [PATCH 04/22] update agp and kotlin apg 4.1.3 -> 4.2.0 kotlin 1.4.32 -> 1.5.0 --- app/src/main/java/org/mosad/teapod/parser/AoDParser.kt | 4 ++-- .../java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt | 4 ++-- build.gradle | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 1963c45..9bce88a 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -177,7 +177,7 @@ object AoDParser { itemMediaList.clear() mediaList.clear() resAnimes.select("div.animebox").forEach { - val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") { + val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { MediaType.TVSHOW } else { MediaType.MOVIE @@ -347,7 +347,7 @@ object AoDParser { // additional info from the media page res.select("table.vertical-table").select("tr").forEach { row -> - when (row.select("th").text().toLowerCase(Locale.ROOT)) { + when (row.select("th").text().lowercase(Locale.ROOT)) { "produktionsjahr" -> media.info.year = row.select("td").text().toInt() "fsk" -> media.info.age = row.select("td").text().toInt() "episodenanzahl" -> { diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt index 0038afa..2c23bcf 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt @@ -49,14 +49,14 @@ class MediaItemAdapter(private val initMedia: List) : RecyclerView.Ad inner class MediaFilter : Filter() { override fun performFiltering(constraint: CharSequence?): FilterResults { - val filterTerm = constraint.toString().toLowerCase(Locale.ROOT) + val filterTerm = constraint.toString().lowercase(Locale.ROOT) val results = FilterResults() val filteredList = if (filterTerm.isEmpty()) { initMedia } else { initMedia.filter { - it.title.toLowerCase(Locale.ROOT).contains(filterTerm) + it.title.lowercase(Locale.ROOT).contains(filterTerm) } } diff --git a/build.gradle b/build.gradle index 734f524..53a5579 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.32" + ext.kotlin_version = "1.5.0" repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 063b5405fcadbd0e9a67a363bb0e681bb32d21b5 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 9 May 2021 20:31:51 +0200 Subject: [PATCH 05/22] add dev settings gui enable dev settings by clicking the app icon in the about screen 5 times --- app/build.gradle | 2 +- .../mosad/teapod/preferences/Preferences.kt | 14 +++ .../activity/main/fragments/AboutFragment.kt | 37 +++++- .../main/fragments/AccountFragment.kt | 11 ++ .../res/drawable/ic_launcher_foreground.xml | 27 ++-- .../res/drawable/ic_outline_download_24.xml | 10 ++ .../main/res/drawable/ic_outline_info_24.xml | 13 +- .../res/drawable/ic_outline_upload_24.xml | 10 ++ app/src/main/res/layout/fragment_account.xml | 116 +++++++++++++++++- app/src/main/res/values-de-rDE/strings.xml | 7 ++ app/src/main/res/values/strings.xml | 9 ++ 11 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 app/src/main/res/drawable/ic_outline_download_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_upload_24.xml diff --git a/app/build.gradle b/app/build.gradle index 65100d7..e04cb7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,7 +64,7 @@ dependencies { implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt index 0c8b0c2..b5c1d60 100644 --- a/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt +++ b/app/src/main/java/org/mosad/teapod/preferences/Preferences.kt @@ -11,6 +11,8 @@ object Preferences { internal set var autoplay = true internal set + var devSettings = false + internal set var theme = DataTypes.Theme.DARK internal set @@ -39,6 +41,15 @@ object Preferences { this.autoplay = autoplay } + fun saveDevSettings(context: Context, devSettings: Boolean) { + with(getSharedPref(context).edit()) { + putBoolean(context.getString(R.string.save_key_dev_settings), devSettings) + apply() + } + + this.devSettings = devSettings + } + fun saveTheme(context: Context, theme: DataTypes.Theme) { with(getSharedPref(context).edit()) { putString(context.getString(R.string.save_key_theme), theme.toString()) @@ -60,6 +71,9 @@ object Preferences { autoplay = sharedPref.getBoolean( context.getString(R.string.save_key_autoplay), true ) + devSettings = sharedPref.getBoolean( + context.getString(R.string.save_key_dev_settings), false + ) theme = DataTypes.Theme.valueOf( sharedPref.getString( context.getString(R.string.save_key_theme), DataTypes.Theme.DARK.toString() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt index a30d129..8c8da0a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AboutFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.annotation.RawRes import androidx.fragment.app.Fragment import com.afollestad.materialdialogs.MaterialDialog @@ -13,15 +14,21 @@ import org.mosad.teapod.BuildConfig import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentAboutBinding import org.mosad.teapod.databinding.ItemComponentBinding +import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.util.DataTypes.License import org.mosad.teapod.util.ThirdPartyComponent import java.lang.StringBuilder +import java.util.Timer +import kotlin.concurrent.schedule class AboutFragment : Fragment() { - private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod" private lateinit var binding: FragmentAboutBinding + private val teapodRepoUrl = "https://git.mosad.xyz/Seil0/teapod" + private val devClickMax = 5 + private var devClickCount = 0 + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentAboutBinding.inflate(inflater, container, false) return binding.root @@ -52,6 +59,10 @@ class AboutFragment : Fragment() { } private fun initActions() { + binding.imageAppIcon.setOnClickListener { + checkDevSettings() + } + binding.linearSource.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(teapodRepoUrl))) } @@ -64,6 +75,30 @@ class AboutFragment : Fragment() { } } + /** + * check if dev settings shall be enabled + */ + private fun checkDevSettings() { + // if the dev settings are already enabled show a toast + if (Preferences.devSettings) { + Toast.makeText(context, getString(R.string.dev_settings_already), Toast.LENGTH_SHORT).show() + return + } + + // reset dev settings count after 5 seconds + if (devClickCount == 0) { + Timer("", false).schedule(5000) { + devClickCount = 0 + } + } + devClickCount++ + + if (devClickCount == devClickMax) { + Preferences.saveDevSettings(requireContext(), true) + Toast.makeText(context, getString(R.string.dev_settings_enabled), Toast.LENGTH_SHORT).show() + } + } + private fun getThirdPartyComponents(): List { return listOf( ThirdPartyComponent("AndroidX", "", "The Android Open Source Project", diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 84a9044..5f0b4ae 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -7,6 +7,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice @@ -54,6 +55,8 @@ class AccountFragment : Fragment() { binding.switchSecondary.isChecked = Preferences.preferSecondary binding.switchAutoplay.isChecked = Preferences.autoplay + binding.linearDevSettings.isVisible = Preferences.devSettings + initActions() } @@ -81,6 +84,14 @@ class AccountFragment : Fragment() { binding.switchAutoplay.setOnClickListener { Preferences.saveAutoplay(requireContext(), binding.switchAutoplay.isChecked) } + + binding.linearExportData.setOnClickListener { + println("TODO") + } + + binding.linearImportData.setOnClickListener { + println("TODO") + } } private fun showLoginDialog(firstTry: Boolean) { diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 6401f43..3e8d0ed 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -3,17 +3,18 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> - - - + + + diff --git a/app/src/main/res/drawable/ic_outline_download_24.xml b/app/src/main/res/drawable/ic_outline_download_24.xml new file mode 100644 index 0000000..b987952 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_download_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_info_24.xml b/app/src/main/res/drawable/ic_outline_info_24.xml index 24bd840..b08088e 100644 --- a/app/src/main/res/drawable/ic_outline_info_24.xml +++ b/app/src/main/res/drawable/ic_outline_info_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_outline_upload_24.xml b/app/src/main/res/drawable/ic_outline_upload_24.xml new file mode 100644 index 0000000..b343e7c --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_upload_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index ec914a6..54fda09 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -220,7 +220,7 @@ android:padding="7dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Design Hell Dunkel + Entwickler Einstellungen + Daten exportieren + Speichere "meine Liste" in eine Datei + Daten importieren + Lade "meine Liste" aus einer Datei Version @@ -55,6 +60,8 @@ Eine inoffizielle App für Anime on Demand. Lizenzen von Drittanbietern © %1$s %2$s unter %3$s + Du bist jetzt ein Entwickler + Du bist schon ein Entwickler Player schließen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e97bfc8..712e8ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,12 @@ Theme Light Dark + Developer Settings + export data + export my list to a file + import data + import my list from a file + Version @@ -71,6 +77,8 @@ This product uses the TMDb API but is not endorsed or certified by TMDb. Third Party Licenses © %1$s %2$s under %3$s + You are now a developer + You are already a developer close player @@ -116,6 +124,7 @@ org.mosad.teapod.user_password org.mosad.teapod.prefer_secondary org.mosad.teapod.autoplay + org.mosad.teapod.dev.settings org.mosad.teapod.theme From 68d462eeee50eb2059db7a8262ec0d74587ed788 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 14 May 2021 23:37:59 +0200 Subject: [PATCH 06/22] update android gradle plugin --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 53a5579..e016c38 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 7ce67f57cd88846301f9119067be1c26e59d497c Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 26 May 2021 19:46:46 +0200 Subject: [PATCH 07/22] add export/import of my list fixes #39 --- app/build.gradle | 4 +- .../main/fragments/AccountFragment.kt | 47 +++++++++++++++-- .../mosad/teapod/util/StorageController.kt | 51 ++++++++++++++++++- app/src/main/res/values-de-rDE/strings.xml | 1 + app/src/main/res/values/strings.xml | 5 +- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e04cb7c..11ce402 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "org.mosad.teapod" minSdkVersion 23 targetSdkVersion 30 - versionCode 4100 //00.04.100 - versionName "0.4.1" + versionCode 4180 //00.04.100 + versionName "0.4.2-alpha1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 5f0b4ae..87dc432 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -1,5 +1,6 @@ package org.mosad.teapod.ui.activity.main.fragments +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle @@ -7,6 +8,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.afollestad.materialdialogs.MaterialDialog @@ -14,20 +16,24 @@ import com.afollestad.materialdialogs.list.listItemsSingleChoice import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.mosad.teapod.BuildConfig -import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentAccountBinding import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.Preferences +import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.util.DataTypes.Theme +import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.showFragment class AccountFragment : Fragment() { private lateinit var binding: FragmentAccountBinding + private val exportFileCode = 1111 + private val importFileCode = 1122 + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentAccountBinding.inflate(inflater, container, false) return binding.root @@ -60,6 +66,30 @@ class AccountFragment : Fragment() { initActions() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // return if the result was not ok + if (resultCode != Activity.RESULT_OK) { + Log.e(javaClass.name, "Error while computing result. Result code is: $resultCode") + return + } + + when(requestCode) { + exportFileCode -> data?.data?.also { uri -> + StorageController.exportMyList(requireContext(), uri) + } + + importFileCode -> data?.data?.also { uri -> + val success = StorageController.importMyList(requireContext(), uri) + if (success == 0) { + Toast.makeText( + context, getString(R.string.import_data_success), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + private fun initActions() { binding.linearAccountLogin.setOnClickListener { showLoginDialog(true) @@ -86,11 +116,20 @@ class AccountFragment : Fragment() { } binding.linearExportData.setOnClickListener { - println("TODO") + val i = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/json" + putExtra(Intent.EXTRA_TITLE, "my-list.json") + } + startActivityForResult(i, exportFileCode) } binding.linearImportData.setOnClickListener { - println("TODO") + val i = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + } + startActivityForResult(i, importFileCode) } } @@ -120,7 +159,7 @@ class AccountFragment : Fragment() { when(index) { 0 -> Preferences.saveTheme(context, Theme.LIGHT) 1 -> Preferences.saveTheme(context, Theme.DARK) - else -> Preferences.saveTheme(context, Theme.LIGHT) + else -> Preferences.saveTheme(context, Theme.DARK) } (activity as MainActivity).restart() diff --git a/app/src/main/java/org/mosad/teapod/util/StorageController.kt b/app/src/main/java/org/mosad/teapod/util/StorageController.kt index ac4309f..291ac81 100644 --- a/app/src/main/java/org/mosad/teapod/util/StorageController.kt +++ b/app/src/main/java/org/mosad/teapod/util/StorageController.kt @@ -1,12 +1,18 @@ package org.mosad.teapod.util import android.content.Context +import android.net.Uri import android.util.Log +import android.widget.Toast import com.google.gson.Gson import com.google.gson.JsonParser import kotlinx.coroutines.* +import org.mosad.teapod.R import java.io.File +import java.io.FileReader +import java.io.FileWriter import java.lang.Exception +import java.net.URI /** * This controller contains the logic for permanently saved data. @@ -19,6 +25,10 @@ object StorageController { val myList = ArrayList() // a list of saved mediaIds fun load(context: Context) { + loadMyList(context) + } + + fun loadMyList(context: Context) { val file = File(context.filesDir, fileNameMyList) if (!file.exists()) runBlocking { saveMyList(context).join() } @@ -30,7 +40,6 @@ object StorageController { myList.clear() Log.e(javaClass.name, "Parsing of My-List failed.") } - } fun saveMyList(context: Context): Job { @@ -41,4 +50,44 @@ object StorageController { } } + fun exportMyList(context: Context, uri: Uri) { + try { + context.contentResolver.openFileDescriptor(uri, "w")?.use { + FileWriter(it.fileDescriptor).use { writer -> + writer.write(Gson().toJson(myList.distinct())) + } + } + } catch (ex: Exception) { + Log.e(javaClass.name, "Exporting my list failed.", ex) + } + } + + /** + * import my list from a (previously exported) json file + * @param context the current context + * @param uri the uri of the selected file + * @return 0 if import was successfull, else 1 + */ + fun importMyList(context: Context, uri: Uri): Int { + try { + val text = context.contentResolver.openFileDescriptor(uri, "r")?.use { + FileReader(it.fileDescriptor).use { reader -> + reader.readText() + } + } + + myList.clear() + myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct()) + + // after the list has been imported also save it + saveMyList(context) + } catch (ex: Exception) { + myList.clear() + Log.e(javaClass.name, "Importing my list failed.", ex) + return 1 + } + + return 0 + } + } \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index b1d7d0b..b2adda2 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -51,6 +51,7 @@ Speichere "meine Liste" in eine Datei Daten importieren Lade "meine Liste" aus einer Datei + "meine Liste" erfolgreich importiert Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 712e8ac..8c7fbed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,9 +59,10 @@ Dark Developer Settings export data - export my list to a file + export "my list" to a file import data - import my list from a file + import "my list" from a file + imported "my list" successfully From a3a89c6b64965381b2edb316c72ff64f4307a483 Mon Sep 17 00:00:00 2001 From: Jannik Date: Thu, 27 May 2021 19:50:00 +0200 Subject: [PATCH 08/22] don't use deprecated startActivityForResult() use registerForActivityResult() instead --- .../main/fragments/AccountFragment.kt | 53 +++++++++---------- app/src/main/res/values-de-rDE/strings.xml | 6 +-- app/src/main/res/values/strings.xml | 6 +-- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 87dc432..eb8000f 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -9,6 +9,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.afollestad.materialdialogs.MaterialDialog @@ -31,8 +32,27 @@ class AccountFragment : Fragment() { private lateinit var binding: FragmentAccountBinding - private val exportFileCode = 1111 - private val importFileCode = 1122 + private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.also { uri -> + StorageController.exportMyList(requireContext(), uri) + } + } + } + + private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.also { uri -> + val success = StorageController.importMyList(requireContext(), uri) + if (success == 0) { + Toast.makeText( + context, getString(R.string.import_data_success), + Toast.LENGTH_SHORT + ).show() + } + } + } + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentAccountBinding.inflate(inflater, container, false) @@ -66,30 +86,6 @@ class AccountFragment : Fragment() { initActions() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - // return if the result was not ok - if (resultCode != Activity.RESULT_OK) { - Log.e(javaClass.name, "Error while computing result. Result code is: $resultCode") - return - } - - when(requestCode) { - exportFileCode -> data?.data?.also { uri -> - StorageController.exportMyList(requireContext(), uri) - } - - importFileCode -> data?.data?.also { uri -> - val success = StorageController.importMyList(requireContext(), uri) - if (success == 0) { - Toast.makeText( - context, getString(R.string.import_data_success), - Toast.LENGTH_SHORT - ).show() - } - } - } - } - private fun initActions() { binding.linearAccountLogin.setOnClickListener { showLoginDialog(true) @@ -121,7 +117,7 @@ class AccountFragment : Fragment() { type = "text/json" putExtra(Intent.EXTRA_TITLE, "my-list.json") } - startActivityForResult(i, exportFileCode) + getUriExport.launch(i) } binding.linearImportData.setOnClickListener { @@ -129,7 +125,7 @@ class AccountFragment : Fragment() { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" } - startActivityForResult(i, importFileCode) + getUriImport.launch(i) } } @@ -166,4 +162,5 @@ class AccountFragment : Fragment() { } } } + } \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index b2adda2..7525379 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -48,10 +48,10 @@ Dunkel Entwickler Einstellungen Daten exportieren - Speichere "meine Liste" in eine Datei + Speichere "Meine Liste" in eine Datei Daten importieren - Lade "meine Liste" aus einer Datei - "meine Liste" erfolgreich importiert + Lade "Meine Liste" aus einer Datei + "Meine Liste" erfolgreich importiert Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c7fbed..874ede9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,10 +59,10 @@ Dark Developer Settings export data - export "my list" to a file + export "My list" to a file import data - import "my list" from a file - imported "my list" successfully + import "My list" from a file + imported "My list" successfully From 46e3d1f1b6946632446f48d3045fb089b8140843 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 4 Jun 2021 00:18:33 +0200 Subject: [PATCH 09/22] don't use "save" when selecting the media language, use "apply" instead --- app/src/main/res/layout/player_language_settings.xml | 2 +- app/src/main/res/values-de-rDE/strings.xml | 5 +++-- app/src/main/res/values/strings.xml | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/player_language_settings.xml b/app/src/main/res/layout/player_language_settings.xml index bee72f0..b887b41 100644 --- a/app/src/main/res/layout/player_language_settings.xml +++ b/app/src/main/res/layout/player_language_settings.xml @@ -86,7 +86,7 @@ android:id="@+id/button_select" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/save" + android:text="@string/apply" android:textAllCaps="false" android:textColor="@color/themePrimaryDark" android:textSize="16sp" diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 7525379..1efd478 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -85,9 +85,10 @@ Login nicht erfolgreich! Stelle sicher das deine Login-Daten korrekt sind und versuche es erneut. - speichern + Speichern + Übernehmen @android:string/cancel - lädt… + Lädt… Anmelden fehlgeschlagen Der Server scheint langsam zu antworten. Bitte versuche es später noch einmal. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 874ede9..b375f81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,9 +106,10 @@ Could not login! Make sure Username and Password are correct and try again. - save + Save @android:string/cancel - loading… + Apply + Loading… Login failed Looks like the server is taking to long to respond. Please try again later. From 5e48e724a75639660bdfea90a7cdf3639c0403d4 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 6 Jun 2021 17:54:19 +0200 Subject: [PATCH 10/22] update some libraries & coroutines 1.5.0 * androidx.core 1.3.2 -> 1.5.0 * androidx.appcompat 1.2.0 -> 1.3.0 * gson 2.8.6 -> 2.8.7 * coroutines-android 1.4.3 -> 1.5.0 * don't use GlobalScope, use lifecycleScope and vieModelScope instead. This fixes a few issues when fragments where destroied befor the coroutine finished. * gradle wrapper 7.0 -> 7.9.2 --- app/build.gradle | 10 +- .../java/org/mosad/teapod/parser/AoDParser.kt | 431 +++++++++--------- .../teapod/ui/activity/main/MainActivity.kt | 12 +- .../main/fragments/AccountFragment.kt | 4 +- .../activity/main/fragments/HomeFragment.kt | 9 +- .../main/fragments/LibraryFragment.kt | 22 +- .../activity/main/fragments/MediaFragment.kt | 16 +- .../activity/main/fragments/SearchFragment.kt | 7 +- .../ui/activity/onboarding/OnLoginFragment.kt | 3 +- .../ui/activity/player/PlayerActivity.kt | 4 +- .../ui/activity/player/PlayerViewModel.kt | 6 +- .../mosad/teapod/util/StorageController.kt | 6 +- .../mosad/teapod/util/TMDBApiController.kt | 72 ++- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 305 insertions(+), 301 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 11ce402..9f30c77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,18 +41,20 @@ android { dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'com.google.android.material:material:1.3.0' - implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.code.gson:gson:2.8.7' implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3' diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 9bce88a..d68a8fb 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -99,10 +99,12 @@ object AoDParser { /** * initially load all media and home screen data */ - fun initialLoading() = listOf( - loadHome(), - listAnimes() - ) + suspend fun initialLoading() { + coroutineScope { + launch { loadHome() } + launch { listAnimes() } + } + } /** * get a media by it's ID (int) @@ -121,15 +123,16 @@ object AoDParser { /** * get subscription info from aod website, remove "Anime-Abo" Prefix and trim */ - fun getSubscriptionInfoAsync(): Deferred { - return GlobalScope.async(Dispatchers.IO) { - // get the subscription page - val res = Jsoup.connect(baseUrl + subscriptionPath) - .cookies(sessionCookies) - .get() + suspend fun getSubscriptionInfoAsync(): Deferred { + return coroutineScope { + async(Dispatchers.IO) { + val res = Jsoup.connect(baseUrl + subscriptionPath) + .cookies(sessionCookies) + .get() - return@async res.select("a:contains(Anime-Abo)").text() - .removePrefix("Anime-Abo").trim() + return@async res.select("a:contains(Anime-Abo)").text() + .removePrefix("Anime-Abo").trim() + } } } @@ -137,7 +140,7 @@ object AoDParser { return baseUrl + subscriptionPath } - fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { + suspend fun markAsWatched(mediaId: Int, episodeId: Int) { val episode = getMediaById(mediaId).getEpisodeById(episodeId) episode.watched = true sendCallback(episode.watchedCallback) @@ -146,137 +149,145 @@ object AoDParser { } // TODO don't use jsoup here - private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) { - val headers = mutableMapOf( - Pair("Accept", "application/json, text/javascript, */*; q=0.01"), - Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), - Pair("Accept-Encoding", "gzip, deflate, br"), - Pair("X-CSRF-Token", csrfToken), - Pair("X-Requested-With", "XMLHttpRequest"), - ) + private suspend fun sendCallback(callbackPath: String) = coroutineScope { + launch(Dispatchers.IO) { + val headers = mutableMapOf( + Pair("Accept", "application/json, text/javascript, */*; q=0.01"), + Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), + Pair("Accept-Encoding", "gzip, deflate, br"), + Pair("X-CSRF-Token", csrfToken), + Pair("X-Requested-With", "XMLHttpRequest"), + ) - try { - Jsoup.connect(baseUrl + callbackPath) - .ignoreContentType(true) - .cookies(sessionCookies) - .headers(headers) - .execute() - } catch (ex: IOException) { - Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) + try { + Jsoup.connect(baseUrl + callbackPath) + .ignoreContentType(true) + .cookies(sessionCookies) + .headers(headers) + .execute() + } catch (ex: IOException) { + Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) + } } - } /** * load all media from aod into itemMediaList and mediaList + * TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio? */ - private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) { - val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() - //println(resAnimes) + private suspend fun listAnimes() = withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { + val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() + //println(resAnimes) - itemMediaList.clear() - mediaList.clear() - resAnimes.select("div.animebox").forEach { - val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { - MediaType.TVSHOW - } else { - MediaType.MOVIE + itemMediaList.clear() + mediaList.clear() + resAnimes.select("div.animebox").forEach { + val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { + MediaType.TVSHOW + } else { + MediaType.MOVIE + } + val mediaTitle = it.select("h3.animebox-title").text() + val mediaLink = it.select("p.animebox-link").select("a").attr("href") + val mediaImage = it.select("p.animebox-image").select("img").attr("src") + val mediaShortText = it.select("p.animebox-shorttext").text() + val mediaId = mediaLink.substringAfterLast("/").toInt() + + itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + mediaList.add(Media(mediaId, mediaLink, type).apply { + info.title = mediaTitle + info.posterUrl = mediaImage + info.shortDesc = mediaShortText + }) } - val mediaTitle = it.select("h3.animebox-title").text() - val mediaLink = it.select("p.animebox-link").select("a").attr("href") - val mediaImage = it.select("p.animebox-image").select("img").attr("src") - val mediaShortText = it.select("p.animebox-shorttext").text() - val mediaId = mediaLink.substringAfterLast("/").toInt() - itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) - mediaList.add(Media(mediaId, mediaLink, type).apply { - info.title = mediaTitle - info.posterUrl = mediaImage - info.shortDesc = mediaShortText - }) + Log.i(javaClass.name, "Total library size is: ${mediaList.size}") } - - Log.i(javaClass.name, "Total library size is: ${mediaList.size}") } /** * load new episodes, titles and highlights */ - private fun loadHome() = GlobalScope.launch(Dispatchers.IO) { - val resHome = Jsoup.connect(baseUrl).get() + private suspend fun loadHome() = withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { + val resHome = Jsoup.connect(baseUrl).get() - // get highlights from AoD - highlightsList.clear() - resHome.select("#aod-highlights").select("div.news-item").forEach { - val mediaId = it.select("div.news-item-text").select("a.serienlink") - .attr("href").substringAfterLast("/").toIntOrNull() - val mediaTitle = it.select("div.news-title").select("h2").text() - val mediaImage = it.select("img").attr("src") + // get highlights from AoD + highlightsList.clear() + resHome.select("#aod-highlights").select("div.news-item").forEach { + val mediaId = it.select("div.news-item-text").select("a.serienlink") + .attr("href").substringAfterLast("/").toIntOrNull() + val mediaTitle = it.select("div.news-title").select("h2").text() + val mediaImage = it.select("img").attr("src") - if (mediaId != null) { - highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get all new episodes from AoD - newEpisodesList.clear() - resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" + // get all new episodes from AoD + newEpisodesList.clear() + resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" - if (mediaId != null) { - newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get new simulcasts from AoD - newSimulcastsList.clear() - resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get new simulcasts from AoD + newSimulcastsList.clear() + resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get new titles from AoD - newTitlesList.clear() - resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get new titles from AoD + newTitlesList.clear() + resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // get top ten from AoD - topTenList.clear() - resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() + // get top ten from AoD + topTenList.clear() + resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() - if (mediaId != null) { - topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + if (mediaId != null) { + topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) + } } - } - // if highlights is empty, add a random new title - if (highlightsList.isEmpty()) { - if (newTitlesList.isNotEmpty()) { - highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) - } else { - highlightsList.add(ItemMedia(0,"", "")) + // if highlights is empty, add a random new title + if (highlightsList.isEmpty()) { + if (newTitlesList.isNotEmpty()) { + highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) + } else { + highlightsList.add(ItemMedia(0,"", "")) + } } + + Log.i(javaClass.name, "loaded home") } } @@ -286,112 +297,114 @@ object AoDParser { * load streams for the media path, movies have one episode * @param media is used as call ba reference */ - private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) { - if (sessionCookies.isEmpty()) login() + private suspend fun loadStreams(media: Media) = coroutineScope { + launch(Dispatchers.IO) { + if (sessionCookies.isEmpty()) login() - if (!loginSuccess) { - Log.w(javaClass.name, "Login, was not successful.") - return@launch - } - - // get the media page - val res = Jsoup.connect(baseUrl + media.link) - .cookies(sessionCookies) - .get() - - //println(res) - - if (csrfToken.isEmpty()) { - csrfToken = res.select("meta[name=csrf-token]").attr("content") - //Log.i(javaClass.name, "New csrf token is $csrfToken") - } - - val besides = res.select("div.besides").first() - val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> - parsePlaylistAsync( - streamstarter.attr("data-playlist"), - streamstarter.attr("data-lang") - ) - }.awaitAll() - - playlists.forEach { aod -> - // TODO improve language handling - val locale = when (aod.extLanguage) { - "ger" -> Locale.GERMAN - "jap" -> Locale.JAPANESE - else -> Locale.ROOT + if (!loginSuccess) { + Log.w(javaClass.name, "Login, was not successful.") + return@launch } - aod.playlist.forEach { ep -> - try { - if (media.hasEpisode(ep.mediaid)) { - media.getEpisodeById(ep.mediaid).streams.add( - Stream(ep.sources.first().file, locale) - ) - } else { - media.episodes.add(Episode( - id = ep.mediaid, - streams = mutableListOf(Stream(ep.sources.first().file, locale)), - posterUrl = ep.image, - title = ep.title, - description = ep.description, - number = getNumberFromTitle(ep.title, media.type) - )) - } - } catch (ex: Exception) { - Log.w(javaClass.name, "Could not parse episode information.", ex) + // get the media page + val res = Jsoup.connect(baseUrl + media.link) + .cookies(sessionCookies) + .get() + + //println(res) + + if (csrfToken.isEmpty()) { + csrfToken = res.select("meta[name=csrf-token]").attr("content") + //Log.i(javaClass.name, "New csrf token is $csrfToken") + } + + val besides = res.select("div.besides").first() + val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> + parsePlaylistAsync( + streamstarter.attr("data-playlist"), + streamstarter.attr("data-lang") + ) + }.awaitAll() + + playlists.forEach { aod -> + // TODO improve language handling + val locale = when (aod.extLanguage) { + "ger" -> Locale.GERMAN + "jap" -> Locale.JAPANESE + else -> Locale.ROOT } - } - } - Log.i(javaClass.name, "Loaded playlists successfully") - // additional info from the media page - res.select("table.vertical-table").select("tr").forEach { row -> - when (row.select("th").text().lowercase(Locale.ROOT)) { - "produktionsjahr" -> media.info.year = row.select("td").text().toInt() - "fsk" -> media.info.age = row.select("td").text().toInt() - "episodenanzahl" -> { - media.info.episodesCount = row.select("td").text() - .substringBefore("/") - .filter { it.isDigit() } - .toInt() - } - } - } - - // similar titles from media page - media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { - val mediaId = it.select("a.thumbs").attr("href") - .substringAfterLast("/").toIntOrNull() - val mediaImage = it.select("a.thumbs > img").attr("src") - val mediaTitle = it.select("a").text() - - if (mediaId != null) { - ItemMedia(mediaId, mediaTitle, mediaImage) - } else { - null - } - } - - // additional information for tv shows the episode title (description) is loaded from the "api" - if (media.type == MediaType.TVSHOW) { - res.select("div.three-box-container > div.episodebox").forEach { episodebox -> - // make sure the episode has a streaming link - if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { - val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() - val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() - val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") - val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() - - media.episodes.firstOrNull { it.id == episodeId }?.apply { - shortDesc = episodeShortDesc - watched = episodeWatched - watchedCallback = episodeWatchedCallback + aod.playlist.forEach { ep -> + try { + if (media.hasEpisode(ep.mediaid)) { + media.getEpisodeById(ep.mediaid).streams.add( + Stream(ep.sources.first().file, locale) + ) + } else { + media.episodes.add(Episode( + id = ep.mediaid, + streams = mutableListOf(Stream(ep.sources.first().file, locale)), + posterUrl = ep.image, + title = ep.title, + description = ep.description, + number = getNumberFromTitle(ep.title, media.type) + )) + } + } catch (ex: Exception) { + Log.w(javaClass.name, "Could not parse episode information.", ex) } } } + Log.i(javaClass.name, "Loaded playlists successfully") + + // additional info from the media page + res.select("table.vertical-table").select("tr").forEach { row -> + when (row.select("th").text().lowercase(Locale.ROOT)) { + "produktionsjahr" -> media.info.year = row.select("td").text().toInt() + "fsk" -> media.info.age = row.select("td").text().toInt() + "episodenanzahl" -> { + media.info.episodesCount = row.select("td").text() + .substringBefore("/") + .filter { it.isDigit() } + .toInt() + } + } + } + + // similar titles from media page + media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { + val mediaId = it.select("a.thumbs").attr("href") + .substringAfterLast("/").toIntOrNull() + val mediaImage = it.select("a.thumbs > img").attr("src") + val mediaTitle = it.select("a").text() + + if (mediaId != null) { + ItemMedia(mediaId, mediaTitle, mediaImage) + } else { + null + } + } + + // additional information for tv shows the episode title (description) is loaded from the "api" + if (media.type == MediaType.TVSHOW) { + res.select("div.three-box-container > div.episodebox").forEach { episodebox -> + // make sure the episode has a streaming link + if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { + val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() + val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() + val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") + val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() + + media.episodes.firstOrNull { it.id == episodeId }?.apply { + shortDesc = episodeShortDesc + watched = episodeWatched + watchedCallback = episodeWatchedCallback + } + } + } + } + Log.i(javaClass.name, "media loaded successfully") } - Log.i(javaClass.name, "media loaded successfully") } /** @@ -402,7 +415,7 @@ object AoDParser { return CompletableDeferred(AoDObject(listOf(), language)) } - return GlobalScope.async(Dispatchers.IO) { + return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) { val headers = mutableMapOf( Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index 19dd297..a301b55 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -32,20 +32,19 @@ import androidx.fragment.app.commit import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss import com.google.android.material.bottomnavigation.BottomNavigationView -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.ActivityMainBinding import org.mosad.teapod.parser.AoDParser -import org.mosad.teapod.ui.activity.player.PlayerActivity import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.Preferences -import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.activity.main.fragments.AccountFragment import org.mosad.teapod.ui.activity.main.fragments.HomeFragment import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment import org.mosad.teapod.ui.activity.main.fragments.SearchFragment import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity +import org.mosad.teapod.ui.activity.player.PlayerActivity +import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.exitAndRemoveTask @@ -138,7 +137,8 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS */ private fun load() { val time = measureTimeMillis { - val loadingJob = AoDParser.initialLoading() // start the initial loading + val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) + .async { AoDParser.initialLoading() } // start the initial loading // load all saved stuff here Preferences.load(this) @@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS } } - runBlocking { loadingJob.joinAll() } // wait for initial loading to finish + runBlocking { loadingJob.await() } // wait for initial loading to finish } Log.i(javaClass.name, "loading and login in $time ms") diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index eb8000f..64c7b89 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -12,9 +12,9 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.mosad.teapod.BuildConfig import org.mosad.teapod.R @@ -64,7 +64,7 @@ class AccountFragment : Fragment() { // load subscription (async) info before anything else binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) - GlobalScope.launch { + lifecycleScope.launch { binding.textAccountSubscription.text = getString( R.string.account_subscription, AoDParser.getSubscriptionInfoAsync().await() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 6e19d11..c81ebb7 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -6,14 +6,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.mosad.teapod.R -import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.databinding.FragmentHomeBinding import org.mosad.teapod.parser.AoDParser +import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.adapter.MediaItemAdapter @@ -40,7 +39,7 @@ class HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch { context?.let { initHighlight() initRecyclerViews() @@ -101,7 +100,7 @@ class HomeFragment : Fragment() { private fun initActions() { binding.buttonPlayHighlight.setOnClickListener { // TODO get next episode - GlobalScope.launch { + lifecycleScope.launch { val media = AoDParser.getMediaById(highlightMedia.id) Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt index 01ab191..f757b7a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt @@ -5,10 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.adapter.MediaItemAdapter @@ -29,18 +27,16 @@ class LibraryFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // init async - GlobalScope.launch { + lifecycleScope.launch { // create and set the adapter, needs context - withContext(Dispatchers.Main) { - context?.let { - adapter = MediaItemAdapter(AoDParser.itemMediaList) - adapter.onItemClick = { mediaId, _ -> - activity?.showFragment(MediaFragment(mediaId)) - } - - binding.recyclerMediaLibrary.adapter = adapter - binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) + context?.let { + adapter = MediaItemAdapter(AoDParser.itemMediaList) + adapter.onItemClick = { mediaId, _ -> + activity?.showFragment(MediaFragment(mediaId)) } + + binding.recyclerMediaLibrary.adapter = adapter + binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt index 6e5f750..a762032 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt @@ -10,20 +10,21 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator import jp.wasabeef.glide.transformations.BlurTransformation -import kotlinx.coroutines.* +import kotlinx.coroutines.launch import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel -import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType - +import org.mosad.teapod.util.Episode +import org.mosad.teapod.util.StorageController /** * The media detail fragment. @@ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } }.attach() - GlobalScope.launch(Dispatchers.Main) { + + lifecycleScope.launch { model.load(mediaId) // load the streams and tmdb for the selected media - if (this@MediaFragment.isAdded) { - updateGUI() - initActions() - } + updateGUI() + initActions() } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt index 57c43b1..b430092 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt @@ -6,7 +6,8 @@ import android.view.View import android.view.ViewGroup import android.widget.SearchView import androidx.fragment.app.Fragment -import kotlinx.coroutines.* +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import org.mosad.teapod.databinding.FragmentSearchBinding import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.decoration.MediaItemDecoration @@ -26,9 +27,8 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - GlobalScope.launch { + lifecycleScope.launch { // create and set the adapter, needs context - withContext(Dispatchers.Main) { context?.let { adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter!!.onItemClick = { mediaId, _ -> @@ -39,7 +39,6 @@ class SearchFragment : Fragment() { binding.recyclerMediaSearch.adapter = adapter binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9)) } - } } initActions() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt index f65ab30..6a329be 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentOnLoginBinding @@ -35,7 +36,7 @@ class OnLoginFragment: Fragment() { EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials binding.buttonLogin.isClickable = false - loginJob = GlobalScope.launch { + loginJob = lifecycleScope.launch { if (AoDParser.login()) { // if login was successful, switch to main if (activity is OnboardingActivity) { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 6c29035..44e3b55 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -19,6 +19,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ui.StyledPlayerControlView @@ -26,7 +27,6 @@ import com.google.android.exoplayer2.util.Util import kotlinx.android.synthetic.main.activity_player.* import kotlinx.android.synthetic.main.player_controls.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.mosad.teapod.R @@ -255,7 +255,7 @@ class PlayerActivity : AppCompatActivity() { } timerUpdates = Timer().scheduleAtFixedRate(0, 500) { - GlobalScope.launch { + lifecycleScope.launch { var btnNextEpIsVisible: Boolean var controlsVisible: Boolean diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 4efb7c4..19a4caf 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.net.Uri import android.util.Log import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import com.google.android.exoplayer2.C import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.SimpleExoPlayer @@ -11,6 +12,7 @@ import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.mosad.teapod.R import org.mosad.teapod.parser.AoDParser @@ -107,7 +109,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) // if episodes has not been watched, mark as watched if (!episode.watched) { - AoDParser.markAsWatched(media.id, episode.id) + viewModelScope.launch { + AoDParser.markAsWatched(media.id, episode.id) + } } } diff --git a/app/src/main/java/org/mosad/teapod/util/StorageController.kt b/app/src/main/java/org/mosad/teapod/util/StorageController.kt index 291ac81..14a0f26 100644 --- a/app/src/main/java/org/mosad/teapod/util/StorageController.kt +++ b/app/src/main/java/org/mosad/teapod/util/StorageController.kt @@ -3,16 +3,12 @@ package org.mosad.teapod.util import android.content.Context import android.net.Uri import android.util.Log -import android.widget.Toast import com.google.gson.Gson import com.google.gson.JsonParser import kotlinx.coroutines.* -import org.mosad.teapod.R import java.io.File import java.io.FileReader import java.io.FileWriter -import java.lang.Exception -import java.net.URI /** * This controller contains the logic for permanently saved data. @@ -45,7 +41,7 @@ object StorageController { fun saveMyList(context: Context): Job { val file = File(context.filesDir, fileNameMyList) - return GlobalScope.launch(Dispatchers.IO) { + return CoroutineScope(Dispatchers.IO).launch { file.writeText(Gson().toJson(myList.distinct())) } } diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt index bb76090..9d51653 100644 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt @@ -24,8 +24,8 @@ class TMDBApiController { val searchTerm = title.replace("(Sub)", "").trim() return when (type) { - MediaType.MOVIE -> searchMovie(searchTerm).await() - MediaType.TVSHOW -> searchTVShow(searchTerm).await() + MediaType.MOVIE -> searchMovie(searchTerm) + MediaType.TVSHOW -> searchTVShow(searchTerm) else -> { Log.e(javaClass.name, "Wrong Type: $type") TMDBResponse() @@ -34,62 +34,56 @@ class TMDBApiController { } - fun searchTVShow(title: String): Deferred { + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") + val response = JsonParser.parseString(url.readText()).asJsonObject + //println(response) - return GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) + return@withContext if (response.get("total_results").asInt > 0) { + response.get("results").asJsonArray.first().asJsonObject.let { + val id = getStringNotNull(it, "id").toInt() + val overview = getStringNotNull(it, "overview") + val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) + val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { - val id = getStringNotNull(it, "id").toInt() - val overview = getStringNotNull(it, "overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - - TMDBResponse(id, "", overview, posterPath, backdropPath) - } - } else { - TMDBResponse() + TMDBResponse(id, "", overview, posterPath, backdropPath) } + } else { + TMDBResponse() } } - fun searchMovie(title: String): Deferred { + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") + val response = JsonParser.parseString(url.readText()).asJsonObject + //println(response) - return GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) + return@withContext if (response.get("total_results").asInt > 0) { + response.get("results").asJsonArray.first().asJsonObject.let { + val id = getStringNotNull(it,"id").toInt() + val overview = getStringNotNull(it,"overview") + val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) + val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) + val runtime = getMovieRuntime(id) - if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { - val id = getStringNotNull(it,"id").toInt() - val overview = getStringNotNull(it,"overview") - val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) - val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) - val runtime = getMovieRuntime(id) - - TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) - } - } else { - TMDBResponse() + TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) } + } else { + TMDBResponse() } } /** * currently only used for runtime, need a rework */ - fun getMovieRuntime(id: Int): Int = runBlocking { + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) { val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language") - GlobalScope.async { - val response = JsonParser.parseString(url.readText()).asJsonObject - - return@async getStringNotNull(response,"runtime").toInt() - }.await() + val response = JsonParser.parseString(url.readText()).asJsonObject + return@withContext getStringNotNull(response,"runtime").toInt() } /** diff --git a/build.gradle b/build.gradle index e016c38..94a91d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.5.0" + ext.kotlin_version = "1.5.10" repositories { google() mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643..0f80bbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0decf317d9eaa7cf65349137ff574c86448e89c9 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 6 Jun 2021 18:06:59 +0200 Subject: [PATCH 11/22] remove some unneeded resources --- app/src/main/res/raw/notices.xml | 68 ---------------------- app/src/main/res/values-de-rDE/strings.xml | 1 - app/src/main/res/values/strings.xml | 4 -- 3 files changed, 73 deletions(-) delete mode 100644 app/src/main/res/raw/notices.xml diff --git a/app/src/main/res/raw/notices.xml b/app/src/main/res/raw/notices.xml deleted file mode 100644 index b888d8e..0000000 --- a/app/src/main/res/raw/notices.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - AndroidX - https://developer.android.com/jetpack/androidx - Copyright The Android Open Source Project - Apache Software License 2.0 - - - Material Components for Android - https://github.com/material-components/material-components-android - Copyright The Android Open Source Project - Apache Software License 2.0 - - - ExoPlayer - https://github.com/google/ExoPlayer - Copyright The Android Open Source Project - Apache Software License 2.0 - - - Gson - https://github.com/google/gson - Copyright 2008 Google Inc. - Apache Software License 2.0 - - - Material design icons - https://github.com/google/material-design-icons - Copyright Google Inc. - Apache Software License 2.0 - - - Material Dialogs - https://github.com/afollestad/material-dialogs - Copyright Aidan Follestad - Apache Software License 2.0 - - - Jsoup - https://jsoup.org/ - Copyright 2009 - 2020 Jonathan Hedley - MIT License - - - kotlinx.coroutines - https://github.com/Kotlin/kotlinx.coroutines - Copyright 2016 - 2019 JetBrains - Apache Software License 2.0 - - - Glide - https://github.com/bumptech/glide - Copyright Google, Inc - BSD 2-Clause License - - - Glide Transformations - https://github.com/wasabeef/glide-transformations - Copyright 2020 Wasabeef - Apache Software License 2.0 - - - The Movie Database API - https://www.themoviedb.org - This product uses the TMDb API but is not endorsed or certified by TMDb - - \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 1efd478..af2731e 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -22,7 +22,6 @@ %d Episode %d Episoden - %1$d Minuten %d Minute %d Minuten diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b375f81..c7ba0b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,7 +29,6 @@ %d episode %d episodes - %1$d Minutes %d Minute %d Minutes @@ -132,7 +131,4 @@ intent_media_id intent_episode_id - state_resume_window - state_resume_position - state_is_playing \ No newline at end of file From 1d071eafdb7ead3fd366afc8582992fb300f24cd Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 12 Jun 2021 20:57:12 +0200 Subject: [PATCH 12/22] add media session & update exo player to 2.14.0 --- app/build.gradle | 11 ++++--- .../ui/activity/player/PlayerActivity.kt | 26 ++++++---------- .../ui/activity/player/PlayerViewModel.kt | 31 +++++++++++++++++-- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9f30c77..b686fa2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdkVersion 23 targetSdkVersion 30 versionCode 4180 //00.04.100 - versionName "0.4.2-alpha1" + versionName "0.4.2-alpha2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() @@ -55,10 +55,11 @@ dependencies { implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.code.gson:gson:2.8.7' - implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3' + implementation 'com.google.android.exoplayer:exoplayer-core:2.14.0' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.0' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.0' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.0' + implementation 'com.google.android.exoplayer:extension-mediasession:2.14.0' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 44e3b55..15916fa 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -26,9 +26,7 @@ import com.google.android.exoplayer2.ui.StyledPlayerControlView import com.google.android.exoplayer2.util.Util import kotlinx.android.synthetic.main.activity_player.* import kotlinx.android.synthetic.main.player_controls.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.mosad.teapod.R import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.ui.components.EpisodesListPlayer @@ -187,7 +185,7 @@ class PlayerActivity : AppCompatActivity() { * set play when ready and listeners */ private fun initExoPlayer() { - model.player.addListener(object : Player.EventListener { + model.player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(state: Int) { super.onPlaybackStateChanged(state) @@ -208,7 +206,7 @@ class PlayerActivity : AppCompatActivity() { } } }) - + // start playing the current episode, after all needed player components have been initialized model.playEpisode(model.currentEpisode, true) } @@ -256,30 +254,26 @@ class PlayerActivity : AppCompatActivity() { timerUpdates = Timer().scheduleAtFixedRate(0, 500) { lifecycleScope.launch { - var btnNextEpIsVisible: Boolean - var controlsVisible: Boolean + val btnNextEpIsVisible = button_next_ep.isVisible + val controlsVisible = controller.isVisible - withContext(Dispatchers.Main) { - if (model.player.duration > 0) { - remainingTime = model.player.duration - model.player.currentPosition - remainingTime = if (remainingTime < 0) 0 else remainingTime - } - btnNextEpIsVisible = button_next_ep.isVisible - controlsVisible = controller.isVisible + if (model.player.duration > 0) { + remainingTime = model.player.duration - model.player.currentPosition + remainingTime = if (remainingTime < 0) 0 else remainingTime } if (remainingTime in 1..20000) { // if the next ep button is not visible, make it visible. Don't show in pip mode if (!btnNextEpIsVisible && model.nextEpisode != null && Preferences.autoplay && !isInPiPMode()) { - withContext(Dispatchers.Main) { showButtonNextEp() } + showButtonNextEp() } } else if (btnNextEpIsVisible) { - withContext(Dispatchers.Main) { hideButtonNextEp() } + hideButtonNextEp() } // if controls are visible, update them if (controlsVisible) { - withContext(Dispatchers.Main) { updateControls() } + updateControls() } } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 19a4caf..8d8c565 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -2,12 +2,15 @@ package org.mosad.teapod.ui.activity.player import android.app.Application import android.net.Uri +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.google.android.exoplayer2.C import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory @@ -23,6 +26,7 @@ import org.mosad.teapod.util.Media import java.util.* import kotlin.collections.ArrayList + /** * PlayerViewModel handles all stuff related to media/episodes. * When currentEpisode is changed the player will start playing it (not initial media), @@ -31,10 +35,13 @@ import kotlin.collections.ArrayList class PlayerViewModel(application: Application) : AndroidViewModel(application) { val player = SimpleExoPlayer.Builder(application).build() - val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) + private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) + private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION") + + lateinit var mStateBuilder: PlaybackStateCompat val currentEpisodeChangedListener = ArrayList<() -> Unit>() - val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN + private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN var media: Media = Media(-1, "", DataTypes.MediaType.OTHER) internal set @@ -45,13 +52,30 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) var currentLanguage: Locale = Locale.ROOT internal set + init { + initMediaSession() + } + override fun onCleared() { super.onCleared() + + mediaSession.release() player.release() Log.d(javaClass.name, "Released player") } + /** + * set the media session to active + * create a media session connector to set title and description + */ + private fun initMediaSession() { + val mediaSessionConnector = MediaSessionConnector(mediaSession) + mediaSessionConnector.setPlayer(player) + + mediaSession.isActive = true + } + fun loadMedia(mediaId: Int, episodeId: Int) { runBlocking { media = AoDParser.getMediaById(mediaId) @@ -115,6 +139,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) } } + /** + * change the players media source and start playback + */ fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) { if (replace || player.contentDuration == C.TIME_UNSET) { player.setMediaSource(source) From 44d1825095112e4cf8b8116429931bcf97789f22 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 12 Jun 2021 21:02:09 +0200 Subject: [PATCH 13/22] minor code clean up --- .../java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 8d8c565..93abe76 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -38,8 +38,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) private val dataSourceFactory = DefaultDataSourceFactory(application, Util.getUserAgent(application, "Teapod")) private val mediaSession = MediaSessionCompat(application, "TEAPOD_PLAYER_SESSION") - lateinit var mStateBuilder: PlaybackStateCompat - val currentEpisodeChangedListener = ArrayList<() -> Unit>() private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN From 164db8ebd12c85f427b01f63b7e32a4428e04ea0 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 12 Jun 2021 21:03:19 +0200 Subject: [PATCH 14/22] remove unneeded import --- .../java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 93abe76..5dcd69f 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -3,7 +3,6 @@ package org.mosad.teapod.ui.activity.player import android.app.Application import android.net.Uri import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope @@ -26,7 +25,6 @@ import org.mosad.teapod.util.Media import java.util.* import kotlin.collections.ArrayList - /** * PlayerViewModel handles all stuff related to media/episodes. * When currentEpisode is changed the player will start playing it (not initial media), From 8afbae1e1a881c7c06792f1f297aede5c28feacb Mon Sep 17 00:00:00 2001 From: Jannik Date: Thu, 17 Jun 2021 19:36:13 +0200 Subject: [PATCH 15/22] set pip source hint & update exo player * exo player 2.14.0 -> 2.14.1 --- app/build.gradle | 10 +++++----- .../mosad/teapod/ui/activity/player/PlayerActivity.kt | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b686fa2..b0b1ebd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,11 +55,11 @@ dependencies { implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.code.gson:gson:2.8.7' - implementation 'com.google.android.exoplayer:exoplayer-core:2.14.0' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.0' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.0' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.0' - implementation 'com.google.android.exoplayer:extension-mediasession:2.14.0' + implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.1' + implementation 'com.google.android.exoplayer:extension-mediasession:2.14.1' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index 15916fa..66728f4 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -7,6 +7,7 @@ import android.app.PictureInPictureParams import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Rect import android.os.Build import android.os.Bundle import android.util.Log @@ -145,8 +146,15 @@ class PlayerActivity : AppCompatActivity() { } else { val width = model.player.videoFormat?.width ?: 0 val height = model.player.videoFormat?.height ?: 0 + val contentFrame: View = video_view.findViewById(R.id.exo_content_frame) + val contentRect = with(contentFrame) { + val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow) + Rect(x, y, x + width, y + height) + } + val params = PictureInPictureParams.Builder() .setAspectRatio(Rational(width, height)) + .setSourceRectHint(contentRect) .build() enterPictureInPictureMode(params) } From 03e9c3dae5ed355cd21cb1c3b6e83222f71ebb67 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 3 Jul 2021 13:36:15 +0200 Subject: [PATCH 16/22] fix crash on myList element not present in overall itemMediaList fixes #42 --- .../activity/main/fragments/HomeFragment.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index c81ebb7..1f45bcc 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -72,12 +72,7 @@ class HomeFragment : Fragment() { binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9)) // my list - val myListMedia = StorageController.myList.map { elementId -> - AoDParser.itemMediaList.first { - elementId == it.id - } - } - adapterMyList = MediaItemAdapter(myListMedia) + adapterMyList = MediaItemAdapter(mapMyListToItemMedia()) binding.recyclerMyList.adapter = adapterMyList // new episodes @@ -153,14 +148,19 @@ class HomeFragment : Fragment() { * * only update actual change and not all data (performance) */ fun updateMyListMedia() { - val myListMedia = StorageController.myList.map { elementId -> - AoDParser.itemMediaList.first { - elementId == it.id - } - } - - adapterMyList.updateMediaList(myListMedia) + adapterMyList.updateMediaList(mapMyListToItemMedia()) adapterMyList.notifyDataSetChanged() } + private fun mapMyListToItemMedia(): List { + return StorageController.myList.mapNotNull { elementId -> + AoDParser.itemMediaList.firstOrNull { it.id == elementId }.also { + // it the my list entry wasn't found in itemMediaList Log it + if (it == null) { + Log.w(javaClass.name, "The element with the id $elementId was not found.") + } + } + } + } + } \ No newline at end of file From 3fcd1a96b23068480c39db6a63ce9a1a20978ff5 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 3 Jul 2021 13:45:59 +0200 Subject: [PATCH 17/22] update kotlin, some libs & agp * kotlin 1.5.10 -> 1.5.20 * core-ktx 1.5.0 -> 1.6.0 * material-components-android 1.3.0 -> 1.4.0 --- app/build.gradle | 8 ++++---- build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b0b1ebd..a3c27a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' @@ -53,7 +53,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.code.gson:gson:2.8.7' implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1' implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' @@ -68,8 +68,8 @@ dependencies { implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/build.gradle b/build.gradle index 94a91d3..0492e3b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.5.10" + ext.kotlin_version = "1.5.20" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.1' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 55552698770c1171f3e6c927604d20222d89ddc3 Mon Sep 17 00:00:00 2001 From: Jannik Date: Wed, 30 Jun 2021 20:17:15 +0200 Subject: [PATCH 18/22] sort tmdb results with String.compareTo --- .../mosad/teapod/util/TMDBApiController.kt | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt index 9d51653..205d221 100644 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt @@ -38,10 +38,48 @@ class TMDBApiController { private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) + println(url) + println(response) return@withContext if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { + val results = response.get("results").asJsonArray + var result = results.asJsonArray.first() + var bestMatch = Int.MAX_VALUE + + results.forEach { it -> + val name = getStringNotNull(it.asJsonObject, "name") + val rating = name.compareTo(title) + + println("name: $name, title: $title") + + when { + rating < 0 -> { + println("rating < ($rating): $it") + + if ((rating * -1) < bestMatch) { + bestMatch = (rating * -1) + result = it + println("best < ($bestMatch): $it") + } + } + rating == 0 -> { + println("rating == ($rating): $it") + bestMatch = 0 + result = it + println("best == ($bestMatch): $it") + } + else -> { + println("rating > ($rating): $it") + if (rating < bestMatch) { + bestMatch = rating + result = it + println("best > ($bestMatch): $it") + } + } + } + } + + result.asJsonObject.let { val id = getStringNotNull(it, "id").toInt() val overview = getStringNotNull(it, "overview") val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) From e0a6485ed759f9497d8e8fa99ba1e4745bcf7c53 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 3 Jul 2021 15:51:52 +0200 Subject: [PATCH 19/22] tmdb api improvements * sort tmdb results by name * remove season information in media title before searching --- app/build.gradle | 4 +- .../mosad/teapod/util/TMDBApiController.kt | 64 ++++++------------- 2 files changed, 21 insertions(+), 47 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a3c27a1..17b14a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "org.mosad.teapod" minSdkVersion 23 targetSdkVersion 30 - versionCode 4180 //00.04.100 - versionName "0.4.2-alpha2" + versionCode 4190 //00.04.190 + versionName "0.4.2-beta1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt index 205d221..140b1d7 100644 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt @@ -4,9 +4,9 @@ import android.util.Log import com.google.gson.JsonObject import com.google.gson.JsonParser import kotlinx.coroutines.* +import org.mosad.teapod.util.DataTypes.MediaType import java.net.URL import java.net.URLEncoder -import org.mosad.teapod.util.DataTypes.MediaType class TMDBApiController { @@ -21,7 +21,11 @@ class TMDBApiController { private val imageUrl = "https://image.tmdb.org/t/p/w500" suspend fun search(title: String, type: MediaType): TMDBResponse { - val searchTerm = title.replace("(Sub)", "").trim() + // remove unneeded text from the media title before searching + val searchTerm = title.replace("(Sub)", "") + .replace(Regex("-?\\s?[0-9]+.\\s?(Staffel|Season)"), "") + .replace(Regex("(Staffel|Season)\\s?[0-9]+"), "") + .trim() return when (type) { MediaType.MOVIE -> searchMovie(searchTerm) @@ -38,48 +42,14 @@ class TMDBApiController { private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") val response = JsonParser.parseString(url.readText()).asJsonObject - println(url) - println(response) +// println(response) - return@withContext if (response.get("total_results").asInt > 0) { - val results = response.get("results").asJsonArray - var result = results.asJsonArray.first() - var bestMatch = Int.MAX_VALUE + val sortedResults = response.get("results").asJsonArray.toList().sortedBy { + getStringNotNull(it.asJsonObject, "name") + } - results.forEach { it -> - val name = getStringNotNull(it.asJsonObject, "name") - val rating = name.compareTo(title) - - println("name: $name, title: $title") - - when { - rating < 0 -> { - println("rating < ($rating): $it") - - if ((rating * -1) < bestMatch) { - bestMatch = (rating * -1) - result = it - println("best < ($bestMatch): $it") - } - } - rating == 0 -> { - println("rating == ($rating): $it") - bestMatch = 0 - result = it - println("best == ($bestMatch): $it") - } - else -> { - println("rating > ($rating): $it") - if (rating < bestMatch) { - bestMatch = rating - result = it - println("best > ($bestMatch): $it") - } - } - } - } - - result.asJsonObject.let { + return@withContext if (sortedResults.isNotEmpty()) { + sortedResults.first().asJsonObject.let { val id = getStringNotNull(it, "id").toInt() val overview = getStringNotNull(it, "overview") val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) @@ -96,10 +66,14 @@ class TMDBApiController { private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) { val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") val response = JsonParser.parseString(url.readText()).asJsonObject - //println(response) +// println(response) - return@withContext if (response.get("total_results").asInt > 0) { - response.get("results").asJsonArray.first().asJsonObject.let { + val sortedResults = response.get("results").asJsonArray.toList().sortedBy { + getStringNotNull(it.asJsonObject, "name") + } + + return@withContext if (sortedResults.isNotEmpty()) { + sortedResults.first().asJsonObject.let { val id = getStringNotNull(it,"id").toInt() val overview = getStringNotNull(it,"overview") val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) From ba7d82bc2b087032fcddf86fa28a5df6af4355fe Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 3 Jul 2021 15:58:20 +0200 Subject: [PATCH 20/22] replace deprecated OnNavigationItemSelectedListener --- .../java/org/mosad/teapod/ui/activity/main/MainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index a301b55..7aaa2c6 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -31,7 +31,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss -import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationBarView import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.ActivityMainBinding @@ -51,7 +51,7 @@ import org.mosad.teapod.util.exitAndRemoveTask import java.net.SocketTimeoutException import kotlin.system.measureTimeMillis -class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { +class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { private lateinit var binding: ActivityMainBinding private var activeBaseFragment: Fragment = HomeFragment() // the currently active fragment, home at the start @@ -72,7 +72,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS theme.applyStyle(getThemeResource(), true) binding = ActivityMainBinding.inflate(layoutInflater) - binding.navView.setOnNavigationItemSelectedListener(this) + binding.navView.setOnItemSelectedListener(this) setContentView(binding.root) supportFragmentManager.commit { From 664959641f3c61cda32e0740990fca43ea0adb7b Mon Sep 17 00:00:00 2001 From: Jannik Date: Sun, 4 Jul 2021 13:01:49 +0200 Subject: [PATCH 21/22] fix tmdb search for movies movies don't have name but titles --- app/build.gradle | 2 +- .../main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt | 2 +- app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 17b14a6..0fe5d0a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdkVersion 23 targetSdkVersion 30 versionCode 4190 //00.04.190 - versionName "0.4.2-beta1" + versionName "0.4.2-beta2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index 7aaa2c6..b6e1502 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -145,7 +145,7 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen EncryptedPreferences.readCredentials(this) StorageController.load(this) - // show onbaording + // show onboarding if (EncryptedPreferences.password.isEmpty()) { showOnboarding() } else { diff --git a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt index 140b1d7..846d544 100644 --- a/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt +++ b/app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt @@ -69,7 +69,7 @@ class TMDBApiController { // println(response) val sortedResults = response.get("results").asJsonArray.toList().sortedBy { - getStringNotNull(it.asJsonObject, "name") + getStringNotNull(it.asJsonObject, "title") } return@withContext if (sortedResults.isNotEmpty()) { From 4de97ca42eba1bb425950c7198136b6dc6cd0589 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 9 Jul 2021 18:54:44 +0200 Subject: [PATCH 22/22] version 0.4.2 --- app/build.gradle | 6 +++--- fastlane/metadata/android/de/changelogs/4200.txt | 5 +++++ fastlane/metadata/android/en-US/changelogs/4200.txt | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/de/changelogs/4200.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/4200.txt diff --git a/app/build.gradle b/app/build.gradle index 0fe5d0a..c194ff7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "org.mosad.teapod" minSdkVersion 23 targetSdkVersion 30 - versionCode 4190 //00.04.190 - versionName "0.4.2-beta2" + versionCode 4200 //00.04.200 + versionName "0.4.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_time", buildTime() @@ -75,4 +75,4 @@ dependencies { static def buildTime() { return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC")) -} \ No newline at end of file +} diff --git a/fastlane/metadata/android/de/changelogs/4200.txt b/fastlane/metadata/android/de/changelogs/4200.txt new file mode 100644 index 0000000..0747648 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/4200.txt @@ -0,0 +1,5 @@ +* Entwickleroptionen + * Export/Import für "Meine Liste" +* Der Picture in Picture Modus hat nun Controlls (#35) +* Teapod stürtzt nicht mehr ab, wenn ein Element aus "Meine List" nicht geladen werden konnte (#42) +* Staffel-Informationen im Title werden bei der Suche in tmdb ignoriert (#43) diff --git a/fastlane/metadata/android/en-US/changelogs/4200.txt b/fastlane/metadata/android/en-US/changelogs/4200.txt new file mode 100644 index 0000000..4e36761 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4200.txt @@ -0,0 +1,5 @@ +* Developer options + * Export/Import for "My List" +* The Picture in Picture Modus now has Controlls (#35) +* Teapod deosn't crash, if a element from "My List" could not be loaded (#42) +* Season-Information in titles will be ignored, when searching in tmdb (#43)