Compare commits
13 Commits
0.1-alpha3
...
0.1.0
Author | SHA1 | Date | |
---|---|---|---|
a51f4ca490 | |||
4ec5d0fdc4
|
|||
8a516c640d
|
|||
49430e10bf
|
|||
81b041ab61
|
|||
cf6a110455
|
|||
c138ab4587
|
|||
f0ed6aa379
|
|||
a5fffd5d02
|
|||
ff0727da22
|
|||
ce84cb57a8
|
|||
4c274eb062
|
|||
a25ec81f6b
|
@ -12,9 +12,10 @@ A unoffical App for Anime-on-Demand.
|
|||||||
* a alternative/secondary stream is currently not supported (for dub titles the subtitle version is missing)
|
* a alternative/secondary stream is currently not supported (for dub titles the subtitle version is missing)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Home_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Home.png)
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Library_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Library.png)
|
||||||
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Media_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Media.png)
|
||||||
|
[<img src="https://www.mosad.xyz/images/Teapod/Teapod_Search_200px.png" width=180>](https://www.mosad.xyz/images/Teapod/Teapod_Search.png)
|
||||||
|
|
||||||
### License
|
### License
|
||||||
This App is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any way.
|
This App is licensed under the terms and conditions of GPL 3. This Project is not associated with Anime-on-Demand in any way.
|
||||||
|
@ -10,8 +10,8 @@ android {
|
|||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1000 //00.01.0000
|
||||||
versionName "0.1-alpha3"
|
versionName "0.1.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -20,7 +20,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,18 +41,18 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
|
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.1'
|
||||||
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
10
app/proguard-rules.pro
vendored
@ -15,7 +15,17 @@
|
|||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
-keep class org.mosad.teapod.util.** { <fields>; }
|
||||||
|
|
||||||
|
#Gson
|
||||||
|
-keepattributes Signature
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
#misc
|
||||||
|
-dontwarn java.lang.instrument.ClassFileTransformer
|
||||||
|
-dontwarn java.lang.ClassValue
|
||||||
|
@ -11,20 +11,25 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".SplashActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name=".PlayerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="orientation|screenSize|layoutDirection"
|
android:theme="@style/PlayerTheme"
|
||||||
android:theme="@style/AppTheme.MaterialComponents.Light.NoActionBar.FullScreen" />
|
android:configChanges="orientation|screenSize|layoutDirection" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -35,13 +35,9 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
import org.mosad.teapod.ui.fragments.MediaFragment
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.fragments.AccountFragment
|
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
import org.mosad.teapod.ui.fragments.HomeFragment
|
import org.mosad.teapod.ui.fragments.*
|
||||||
import org.mosad.teapod.ui.fragments.LibraryFragment
|
|
||||||
import org.mosad.teapod.ui.fragments.SearchFragment
|
|
||||||
import org.mosad.teapod.ui.fragments.LoadingFragment
|
|
||||||
import org.mosad.teapod.util.StorageController
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.TMDBApiController
|
import org.mosad.teapod.util.TMDBApiController
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@ -109,21 +105,19 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
private fun load() {
|
private fun load() {
|
||||||
// running login and list in parallel does not bring any speed improvements
|
// running login and list in parallel does not bring any speed improvements
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
|
Preferences.load(this)
|
||||||
|
|
||||||
// make sure credentials are set
|
// make sure credentials are set
|
||||||
EncryptedPreferences.readCredentials(this)
|
EncryptedPreferences.readCredentials(this)
|
||||||
if (EncryptedPreferences.password.isEmpty()) {
|
if (EncryptedPreferences.password.isEmpty()) {
|
||||||
showLoginDialog(true)
|
showLoginDialog(true)
|
||||||
} else {
|
} else {
|
||||||
// try to login in, as most sites can only bee loaded once loged in
|
// try to login in, as most sites can only bee loaded once loged in
|
||||||
if (!AoDParser().login()) showLoginDialog(false)
|
if (!AoDParser.login()) showLoginDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageController.load(this)
|
StorageController.load(this)
|
||||||
|
AoDParser.initialLoading()
|
||||||
// initially load all media
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
|
|
||||||
// TODO load home screen, can be parallel to listAnimes
|
|
||||||
}
|
}
|
||||||
Log.i(javaClass.name, "login and list in $time ms")
|
Log.i(javaClass.name, "login and list in $time ms")
|
||||||
}
|
}
|
||||||
@ -142,7 +136,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load the streams for the selected media
|
// load the streams for the selected media
|
||||||
val media = AoDParser().getMediaById(mediaId)
|
val media = AoDParser.getMediaById(mediaId)
|
||||||
val tmdb = TMDBApiController().search(media.info.title, media.type)
|
val tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
val mediaFragment = MediaFragment(media, tmdb)
|
val mediaFragment = MediaFragment(media, tmdb)
|
||||||
@ -168,7 +162,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
LoginDialog(this, firstTry).positiveButton {
|
LoginDialog(this, firstTry).positiveButton {
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
if (!AoDParser().login()) {
|
if (!AoDParser.login()) {
|
||||||
showLoginDialog(false)
|
showLoginDialog(false)
|
||||||
Log.w(javaClass.name, "Login failed, please try again.")
|
Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
}
|
}
|
||||||
|
17
app/src/main/java/org/mosad/teapod/SplashActivity.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package org.mosad.teapod
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Teapod
|
||||||
|
*
|
||||||
|
* Copyright 2020 <seil0@mosad.xyz>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
package org.mosad.teapod.parser
|
package org.mosad.teapod.parser
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -6,32 +28,26 @@ import kotlinx.coroutines.*
|
|||||||
import org.jsoup.Connection
|
import org.jsoup.Connection
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.util.*
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
import org.mosad.teapod.util.Episode
|
|
||||||
import org.mosad.teapod.util.ItemMedia
|
|
||||||
import org.mosad.teapod.util.Media
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
object AoDParser {
|
||||||
* maybe AoDParser as object would be useful
|
|
||||||
*/
|
|
||||||
class AoDParser {
|
|
||||||
|
|
||||||
private val baseUrl = "https://www.anime-on-demand.de"
|
private const val baseUrl = "https://www.anime-on-demand.de"
|
||||||
private val loginPath = "/users/sign_in"
|
private const val loginPath = "/users/sign_in"
|
||||||
private val libraryPath = "/animes"
|
private const val libraryPath = "/animes"
|
||||||
|
|
||||||
private val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
|
||||||
|
|
||||||
companion object {
|
|
||||||
private var csrfToken: String = ""
|
|
||||||
private var sessionCookies = mutableMapOf<String, String>()
|
private var sessionCookies = mutableMapOf<String, String>()
|
||||||
|
private var csrfToken: String = ""
|
||||||
private var loginSuccess = false
|
private var loginSuccess = false
|
||||||
|
|
||||||
val mediaList = arrayListOf<Media>()
|
private val mediaList = arrayListOf<Media>()
|
||||||
val itemMediaList = arrayListOf<ItemMedia>()
|
val itemMediaList = arrayListOf<ItemMedia>()
|
||||||
}
|
val newEpisodesList = arrayListOf<ItemMedia>()
|
||||||
|
|
||||||
fun login(): Boolean = runBlocking {
|
fun login(): Boolean = runBlocking {
|
||||||
|
|
||||||
@ -73,45 +89,26 @@ class AoDParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list all animes from the website
|
* initially load all media and home screen data
|
||||||
|
* -> blocking
|
||||||
*/
|
*/
|
||||||
fun listAnimes(): ArrayList<Media> = runBlocking {
|
fun initialLoading() = runBlocking {
|
||||||
if (sessionCookies.isEmpty()) login()
|
val newEPJob = GlobalScope.async {
|
||||||
|
listNewEpisodes()
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
val resAnimes = Jsoup.connect(baseUrl + libraryPath)
|
|
||||||
.cookies(sessionCookies)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
//println(resAnimes)
|
|
||||||
|
|
||||||
mediaList.clear()
|
|
||||||
resAnimes.select("div.animebox").forEach {
|
|
||||||
val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
val listJob = GlobalScope.async {
|
||||||
|
listAnimes()
|
||||||
return@withContext mediaList
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newEPJob.await()
|
||||||
|
listJob.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a media by it's ID (int)
|
||||||
|
* @return Media
|
||||||
|
*/
|
||||||
fun getMediaById(mediaId: Int): Media {
|
fun getMediaById(mediaId: Int): Media {
|
||||||
val media = mediaList.first { it.id == mediaId }
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
@ -122,142 +119,7 @@ class AoDParser {
|
|||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// TODO don't use jsoup here
|
||||||
* load streams for the media path, movies have one episode
|
|
||||||
* @param media is used as call ba reference
|
|
||||||
*/
|
|
||||||
private fun loadStreams(media: Media) = runBlocking {
|
|
||||||
if (sessionCookies.isEmpty()) login()
|
|
||||||
|
|
||||||
if (!loginSuccess) {
|
|
||||||
Log.w(javaClass.name, "Login, was not successful.")
|
|
||||||
return@runBlocking
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
|
|
||||||
val res = Jsoup.connect(baseUrl + media.link)
|
|
||||||
.cookies(sessionCookies)
|
|
||||||
.get()
|
|
||||||
|
|
||||||
//println(res)
|
|
||||||
|
|
||||||
// parse additional info from the media page
|
|
||||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
|
||||||
when (row.select("th").text().toLowerCase(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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse additional information for tv shows
|
|
||||||
media.episodes = when (media.type) {
|
|
||||||
MediaType.MOVIE -> listOf(Episode())
|
|
||||||
MediaType.TVSHOW -> {
|
|
||||||
res.select("div.three-box-container > div.episodebox").map { episodebox ->
|
|
||||||
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()
|
|
||||||
|
|
||||||
Episode(
|
|
||||||
id = episodeId,
|
|
||||||
shortDesc = episodeShortDesc,
|
|
||||||
watched = episodeWatched,
|
|
||||||
watchedCallback = episodeWatchedCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MediaType.OTHER -> listOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (csrfToken.isEmpty()) {
|
|
||||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
|
||||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO has attr data-lag (ger or jap)
|
|
||||||
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
|
|
||||||
|
|
||||||
if (playlists.size > 0) {
|
|
||||||
loadPlaylist(playlists.first(), csrfToken, media.type, media.episodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* load the playlist path and parse it, read the stream info from json
|
|
||||||
* @param episodes is used as call ba reference
|
|
||||||
*/
|
|
||||||
private fun loadPlaylist(playlistPath: String, csrfToken: String, type: MediaType, episodes: List<Episode>) = runBlocking {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
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"),
|
|
||||||
)
|
|
||||||
|
|
||||||
//println("loading streaminfo with cstf: $csrfToken")
|
|
||||||
|
|
||||||
val res = Jsoup.connect(baseUrl + playlistPath)
|
|
||||||
.ignoreContentType(true)
|
|
||||||
.cookies(sessionCookies)
|
|
||||||
.headers(headers)
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
//println(res.body())
|
|
||||||
|
|
||||||
when (type) {
|
|
||||||
MediaType.MOVIE -> {
|
|
||||||
val movie = JsonParser.parseString(res.body()).asJsonObject
|
|
||||||
.get("playlist").asJsonArray
|
|
||||||
.first().asJsonObject
|
|
||||||
|
|
||||||
movie.get("sources").asJsonArray.first().apply {
|
|
||||||
episodes.first().streamUrl = this.asJsonObject.get("file").asString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaType.TVSHOW -> {
|
|
||||||
val episodesJson = JsonParser.parseString(res.body()).asJsonObject
|
|
||||||
.get("playlist").asJsonArray
|
|
||||||
|
|
||||||
episodesJson.forEach { jsonElement ->
|
|
||||||
val episodeId = jsonElement.asJsonObject.get("mediaid")
|
|
||||||
val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray
|
|
||||||
.first().asJsonObject
|
|
||||||
.get("file").asString
|
|
||||||
val episodeTitle = jsonElement.asJsonObject.get("title").asString
|
|
||||||
val episodePoster = jsonElement.asJsonObject.get("image").asString
|
|
||||||
val episodeDescription = jsonElement.asJsonObject.get("description").asString
|
|
||||||
val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt()
|
|
||||||
|
|
||||||
episodes.first { it.id == episodeId.asInt }.apply {
|
|
||||||
this.title = episodeTitle
|
|
||||||
this.posterUrl = episodePoster
|
|
||||||
this.streamUrl = episodeStream
|
|
||||||
this.description = episodeDescription
|
|
||||||
this.number = episodeNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Log.e(javaClass.name, "Wrong Type, please report this issue.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendCallback(callbackPath: String) = GlobalScope.launch {
|
fun sendCallback(callbackPath: String) = GlobalScope.launch {
|
||||||
val headers = mutableMapOf(
|
val headers = mutableMapOf(
|
||||||
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
|
||||||
@ -281,4 +143,228 @@ class AoDParser {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load all media from aod into itemMediaList and mediaList
|
||||||
|
*/
|
||||||
|
private fun listAnimes() = runBlocking {
|
||||||
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val resAnimes = Jsoup.connect(baseUrl + libraryPath)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
//println(resAnimes)
|
||||||
|
|
||||||
|
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") {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load all new episodes from AoD into newEpisodesList
|
||||||
|
*/
|
||||||
|
private fun listNewEpisodes() = runBlocking {
|
||||||
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val resHome = Jsoup.connect(baseUrl)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
newEpisodesList.clear()
|
||||||
|
resHome.select("div.jcarousel-container-new").select("li").forEach {
|
||||||
|
if (it.select("span").hasClass("neweps")) {
|
||||||
|
val mediaId = it.select("a.thumbs").attr("href")
|
||||||
|
.substringAfterLast("/").toInt()
|
||||||
|
val mediaImage = it.select("a.thumbs > img").attr("src")
|
||||||
|
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
|
||||||
|
|
||||||
|
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load streams for the media path, movies have one episode
|
||||||
|
* @param media is used as call ba reference
|
||||||
|
*/
|
||||||
|
private fun loadStreams(media: Media) = runBlocking {
|
||||||
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
|
if (!loginSuccess) {
|
||||||
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
|
return@runBlocking
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
|
||||||
|
// 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 pl = res.select("input.streamstarter_html5").first()
|
||||||
|
val primary = pl.attr("data-playlist")
|
||||||
|
val secondary = pl.attr("data-otherplaylist")
|
||||||
|
val secondaryIsOmU = secondary.contains("OmU", true)
|
||||||
|
|
||||||
|
// load primary and secondary playlist
|
||||||
|
val primaryPlaylist = parsePlaylistAsync(primary)
|
||||||
|
val secondaryPlaylist = parsePlaylistAsync(secondary)
|
||||||
|
|
||||||
|
primaryPlaylist.await().playlist.forEach { ep ->
|
||||||
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
media.episodes.add(
|
||||||
|
Episode(
|
||||||
|
id = ep.mediaid,
|
||||||
|
priStreamUrl = ep.sources.first().file,
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = epNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loading primary playlist finished")
|
||||||
|
|
||||||
|
secondaryPlaylist.await().playlist.forEach { ep ->
|
||||||
|
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
|
||||||
|
|
||||||
|
if (episode != null) {
|
||||||
|
episode.secStreamUrl = ep.sources.first().file
|
||||||
|
episode.secStreamOmU = secondaryIsOmU
|
||||||
|
} else {
|
||||||
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
media.episodes.add(
|
||||||
|
Episode(
|
||||||
|
id = ep.mediaid,
|
||||||
|
secStreamUrl = ep.sources.first().file,
|
||||||
|
secStreamOmU = secondaryIsOmU,
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = epNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loading secondary plalyist finished")
|
||||||
|
|
||||||
|
// parse additional info from the media page
|
||||||
|
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||||
|
when (row.select("th").text().toLowerCase(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse additional information for tv shows
|
||||||
|
if (media.type == MediaType.TVSHOW) {
|
||||||
|
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* don't use Gson().fromJson() as we don't have any control over the api and it may change
|
||||||
|
*/
|
||||||
|
private fun parsePlaylistAsync(playlistPath: String): Deferred<AoDObject> {
|
||||||
|
if (playlistPath == "[]") {
|
||||||
|
return CompletableDeferred(AoDObject(listOf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return GlobalScope.async {
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
//println("loading streaminfo with cstf: $csrfToken")
|
||||||
|
|
||||||
|
val res = Jsoup.connect(baseUrl + playlistPath)
|
||||||
|
.ignoreContentType(true)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.headers(headers)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
//Gson().fromJson(res.body(), AoDObject::class.java)
|
||||||
|
|
||||||
|
return@async AoDObject(JsonParser.parseString(res.body()).asJsonObject
|
||||||
|
.get("playlist").asJsonArray.map {
|
||||||
|
Playlist(
|
||||||
|
sources = it.asJsonObject.get("sources").asJsonArray.map { source ->
|
||||||
|
Source(source.asJsonObject.get("file").asString)
|
||||||
|
},
|
||||||
|
image = it.asJsonObject.get("image").asString,
|
||||||
|
title = it.asJsonObject.get("title").asString,
|
||||||
|
description = it.asJsonObject.get("description").asString,
|
||||||
|
mediaid = it.asJsonObject.get("mediaid").asInt
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,40 @@
|
|||||||
package org.mosad.teapod.preferences
|
package org.mosad.teapod.preferences
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.mosad.teapod.R
|
||||||
|
|
||||||
object Preferences {
|
object Preferences {
|
||||||
|
|
||||||
var login = ""
|
var preferSecondary = false
|
||||||
internal set
|
|
||||||
var password = ""
|
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
fun savePreferSecondary(context: Context, preferSecondary: Boolean) {
|
||||||
|
val sharedPref = context.getSharedPreferences(
|
||||||
|
context.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
fun saveCredentials(login: String, password: String) {
|
with(sharedPref.edit()) {
|
||||||
this.login = login
|
putBoolean(context.getString(R.string.save_key_prefer_secondary), preferSecondary)
|
||||||
this.password = password
|
apply()
|
||||||
|
|
||||||
// TODO save
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
this.preferSecondary = preferSecondary
|
||||||
// TODO
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initially load the stored values
|
||||||
|
*/
|
||||||
|
fun load(context: Context) {
|
||||||
|
val sharedPref = context.getSharedPreferences(
|
||||||
|
context.getString(R.string.preference_file_key),
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
preferSecondary = sharedPref.getBoolean(
|
||||||
|
context.getString(R.string.save_key_prefer_secondary), false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -13,6 +13,7 @@ import org.mosad.teapod.BuildConfig
|
|||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.preferences.EncryptedPreferences
|
import org.mosad.teapod.preferences.EncryptedPreferences
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.ui.components.LoginDialog
|
import org.mosad.teapod.ui.components.LoginDialog
|
||||||
|
|
||||||
class AccountFragment : Fragment() {
|
class AccountFragment : Fragment() {
|
||||||
@ -26,6 +27,7 @@ class AccountFragment : Fragment() {
|
|||||||
|
|
||||||
text_account_login.text = EncryptedPreferences.login
|
text_account_login.text = EncryptedPreferences.login
|
||||||
text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
text_info_about_desc.text = getString(R.string.info_about_desc, BuildConfig.VERSION_NAME, getString(R.string.build_time))
|
||||||
|
switch_secondary.isChecked = Preferences.preferSecondary
|
||||||
|
|
||||||
initActions()
|
initActions()
|
||||||
}
|
}
|
||||||
@ -51,13 +53,17 @@ class AccountFragment : Fragment() {
|
|||||||
.build()
|
.build()
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch_secondary.setOnClickListener {
|
||||||
|
Preferences.savePreferSecondary(requireContext(), switch_secondary.isChecked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoginDialog(firstTry: Boolean) {
|
private fun showLoginDialog(firstTry: Boolean) {
|
||||||
LoginDialog(requireContext(), firstTry).positiveButton {
|
LoginDialog(requireContext(), firstTry).positiveButton {
|
||||||
EncryptedPreferences.saveCredentials(login, password, context)
|
EncryptedPreferences.saveCredentials(login, password, context)
|
||||||
|
|
||||||
if (!AoDParser().login()) {
|
if (!AoDParser.login()) {
|
||||||
showLoginDialog(false)
|
showLoginDialog(false)
|
||||||
Log.w(javaClass.name, "Login failed, please try again.")
|
Log.w(javaClass.name, "Login failed, please try again.")
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import kotlinx.android.synthetic.main.fragment_home.*
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -20,8 +19,8 @@ import org.mosad.teapod.util.decoration.MediaItemDecoration
|
|||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var adapter: MediaItemAdapter
|
private lateinit var adapterMyList: MediaItemAdapter
|
||||||
private lateinit var layoutManager: LinearLayoutManager
|
private lateinit var adapterNewEpisodes: MediaItemAdapter
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
@ -31,20 +30,19 @@ class HomeFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let {
|
context?.let {
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
|
||||||
recycler_my_list.layoutManager = layoutManager
|
|
||||||
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
|
recycler_my_list.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
updateMyListMedia()
|
updateMyListMedia()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
|
||||||
|
recycler_new_episodes.adapter = adapterNewEpisodes
|
||||||
|
recycler_new_episodes.addItemDecoration(MediaItemDecoration(9))
|
||||||
|
|
||||||
|
initActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,11 +54,17 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = MediaItemAdapter(myListMedia)
|
adapterMyList = MediaItemAdapter(myListMedia)
|
||||||
adapter.onItemClick = { mediaId, _ ->
|
adapterMyList.onItemClick = { mediaId, _ ->
|
||||||
(activity as MainActivity).showMediaFragment(mediaId)
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
recycler_my_list.adapter = adapter
|
recycler_my_list.adapter = adapterMyList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initActions() {
|
||||||
|
adapterNewEpisodes.onItemClick = { mediaId, _ ->
|
||||||
|
(activity as MainActivity).showMediaFragment(mediaId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -26,10 +26,6 @@ class LibraryFragment : Fragment() {
|
|||||||
|
|
||||||
// init async
|
// init async
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let {
|
context?.let {
|
||||||
|
@ -17,7 +17,9 @@ import kotlinx.android.synthetic.main.fragment_media.*
|
|||||||
import org.mosad.teapod.MainActivity
|
import org.mosad.teapod.MainActivity
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
|
import org.mosad.teapod.preferences.Preferences
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
|
import org.mosad.teapod.util.Episode
|
||||||
import org.mosad.teapod.util.Media
|
import org.mosad.teapod.util.Media
|
||||||
import org.mosad.teapod.util.StorageController
|
import org.mosad.teapod.util.StorageController
|
||||||
import org.mosad.teapod.util.TMDBResponse
|
import org.mosad.teapod.util.TMDBResponse
|
||||||
@ -88,8 +90,8 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) :
|
|||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
button_play.setOnClickListener {
|
button_play.setOnClickListener {
|
||||||
when (media.type) {
|
when (media.type) {
|
||||||
MediaType.MOVIE -> playStream(media.episodes.first().streamUrl)
|
MediaType.MOVIE -> playStream(media.episodes.first())
|
||||||
MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl)
|
MediaType.TVSHOW -> playStream(media.episodes.first())
|
||||||
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
else -> Log.e(javaClass.name, "Wrong Type: $media.type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,19 +116,33 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) :
|
|||||||
// set onItemClick only in adapter is initialized
|
// set onItemClick only in adapter is initialized
|
||||||
if (this::adapterRecEpisodes.isInitialized) {
|
if (this::adapterRecEpisodes.isInitialized) {
|
||||||
adapterRecEpisodes.onImageClick = { _, position ->
|
adapterRecEpisodes.onImageClick = { _, position ->
|
||||||
playStream(media.episodes[position].streamUrl)
|
playStream(media.episodes[position])
|
||||||
|
|
||||||
// update watched state
|
// update watched state
|
||||||
AoDParser().sendCallback(media.episodes[position].watchedCallback)
|
AoDParser.sendCallback(media.episodes[position].watchedCallback)
|
||||||
adapterRecEpisodes.updateWatchedState(true, position)
|
adapterRecEpisodes.updateWatchedState(true, position)
|
||||||
adapterRecEpisodes.notifyDataSetChanged()
|
adapterRecEpisodes.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playStream(url: String) {
|
/**
|
||||||
Log.d(javaClass.name, "Playing stream: $url")
|
* Play the media's stream
|
||||||
(activity as MainActivity).startPlayer(url)
|
* If prefer secondary or primary is empty and secondary is present (secStreamOmU),
|
||||||
|
* use the secondary stream. Else, if the primary stream is set use the primary stream.
|
||||||
|
*/
|
||||||
|
private fun playStream(ep: Episode) {
|
||||||
|
val streamUrl = if ((Preferences.preferSecondary || ep.priStreamUrl.isEmpty()) && ep.secStreamOmU) {
|
||||||
|
ep.secStreamUrl
|
||||||
|
} else if (ep.priStreamUrl.isNotEmpty()) {
|
||||||
|
ep.priStreamUrl
|
||||||
|
} else {
|
||||||
|
Log.e(javaClass.name, "No stream url set.")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(javaClass.name, "Playing stream: $streamUrl")
|
||||||
|
(activity as MainActivity).startPlayer(streamUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -26,10 +26,6 @@ class SearchFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (AoDParser.mediaList.isEmpty()) {
|
|
||||||
AoDParser().listAnimes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let {
|
context?.let {
|
||||||
|
@ -27,7 +27,7 @@ data class Media(
|
|||||||
val link: String,
|
val link: String,
|
||||||
val type: DataTypes.MediaType,
|
val type: DataTypes.MediaType,
|
||||||
val info: Info = Info(),
|
val info: Info = Info(),
|
||||||
var episodes: List<Episode> = listOf()
|
var episodes: ArrayList<Episode> = arrayListOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Info(
|
data class Info(
|
||||||
@ -40,10 +40,15 @@ data class Info(
|
|||||||
var episodesCount: Int = 0
|
var episodesCount: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if secStreamOmU == true, then a secondary stream is present
|
||||||
|
*/
|
||||||
data class Episode(
|
data class Episode(
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
var title: String = "",
|
var title: String = "",
|
||||||
var streamUrl: String = "",
|
var priStreamUrl: String = "",
|
||||||
|
var secStreamUrl: String = "",
|
||||||
|
var secStreamOmU: Boolean = false,
|
||||||
var posterUrl: String = "",
|
var posterUrl: String = "",
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var shortDesc: String = "",
|
var shortDesc: String = "",
|
||||||
@ -60,3 +65,17 @@ data class TMDBResponse(
|
|||||||
val backdropUrl: String = "",
|
val backdropUrl: String = "",
|
||||||
var runtime: Int = 0
|
var runtime: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class AoDObject(val playlist: List<Playlist>)
|
||||||
|
|
||||||
|
data class Playlist(
|
||||||
|
val sources: List<Source>,
|
||||||
|
val image: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val mediaid: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val file: String = ""
|
||||||
|
)
|
||||||
|
@ -92,13 +92,8 @@ class TMDBApiController {
|
|||||||
|
|
||||||
GlobalScope.async {
|
GlobalScope.async {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
//println(response)
|
|
||||||
|
|
||||||
val runtime = getStringNotNull(response,"runtime").toInt()
|
return@async getStringNotNull(response,"runtime").toInt()
|
||||||
println(runtime)
|
|
||||||
|
|
||||||
|
|
||||||
return@async runtime
|
|
||||||
}.await()
|
}.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,21 +25,24 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
||||||
val context = holder.view.context
|
val context = holder.view.context
|
||||||
|
val ep = episodes[position]
|
||||||
|
|
||||||
holder.view.text_episode_title.text = context.getString(
|
val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) {
|
||||||
R.string.component_episode_title,
|
context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
|
||||||
episodes[position].number,
|
} else {
|
||||||
episodes[position].description
|
context.getString(R.string.component_episode_title, ep.number, ep.description)
|
||||||
)
|
}
|
||||||
holder.view.text_episode_desc.text = episodes[position].shortDesc
|
|
||||||
|
holder.view.text_episode_title.text = titleText
|
||||||
|
holder.view.text_episode_desc.text = ep.shortDesc
|
||||||
|
|
||||||
if (episodes[position].posterUrl.isNotEmpty()) {
|
if (episodes[position].posterUrl.isNotEmpty()) {
|
||||||
Glide.with(context).load(episodes[position].posterUrl)
|
Glide.with(context).load(ep.posterUrl)
|
||||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||||
.into(holder.view.image_episode)
|
.into(holder.view.image_episode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (episodes[position].watched) {
|
if (ep.watched) {
|
||||||
holder.view.image_watched.setImageDrawable(
|
holder.view.image_watched.setImageDrawable(
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24)
|
||||||
)
|
)
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
12
app/src/main/res/drawable/bg_splash.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:drawable="@android:color/black"/>
|
||||||
|
|
||||||
|
<item android:gravity="center" android:width="144dp" android:height="144dp">
|
||||||
|
<bitmap
|
||||||
|
android:gravity="fill_horizontal|fill_vertical"
|
||||||
|
android:src="@drawable/ic_splash_logo"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</layer-list>
|
10
app/src/main/res/drawable/ic_baseline_subtitles_24.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM4,12h4v2L4,14v-2zM14,18L4,18v-2h10v2zM20,18h-4v-2h4v2zM20,14L10,14v-2h10v2z"/>
|
||||||
|
</vector>
|
19
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:scaleX="0.051679686"
|
||||||
|
android:scaleY="0.051679686"
|
||||||
|
android:translateX="27.54"
|
||||||
|
android:translateY="38.90954">
|
||||||
|
<path
|
||||||
|
android:pathData="m850.19,372.71c87.88,-11.01 119.04,-84.97 123.1,-99.87 4.06,-14.89 24.91,-80.57 11.92,-129.36 -12.99,-48.79 -34.36,-72.36 -58.62,-77.25 -24.25,-4.9 -50.59,10.51 -65,32.81 -14.41,22.3 -14.68,45.14 -14.78,55.29 -0.11,10.15 0.76,23.2 -3.37,33.29 -4.13,10.09 3.23,25.71 6.04,35.23 2.81,9.52 9.67,82.62 5.78,115.57 -3.89,32.95 -5.07,34.29 -5.07,34.29zM0.4,23.58C55.81,77.29 56.45,120.86 56.08,132.92c-0.36,12.06 4.77,130.59 11.47,150.76 4.42,13.3 12.11,50.16 41.78,74.48 25.51,20.91 58.65,31.38 58.65,31.38 0,0 36.42,78.46 78.83,108.64 31.56,22.46 39.61,23.74 46.5,35.55 6.18,10.6 93.56,62.62 275.1,47.23 127.29,-10.79 138.56,-44.3 138.56,-44.3 0,0 49.41,-21.9 101.15,-80.43 12.87,-14.56 4.41,-13.21 28.57,-17.79 24.16,-4.58 138.01,-45.58 170.66,-154.36C1039.99,175.32 1017.81,96.01 994.52,69.12 971.23,42.22 931.6,24.18 912.25,24.93c-18.47,0.71 -44.78,4.24 -80.21,46.87 -35.43,42.62 -28.94,37.4 -39.36,41.73 -6.82,2.83 -5.68,3.91 -26.75,-11.65 -20.23,-14.93 -28.9,-21.24 -43.38,-27.24 -7.96,-3.3 2.05,-5.55 2.59,-19.48 0.54,-13.93 2.4,-23.51 -17.32,-23.77 -19.72,-0.26 -408.02,0.21 -408.02,0.21 0,0 -18.8,-1.29 -7.79,24.82 4.2,9.94 -1.45,6.43 -33.27,25.85 -31.82,19.42 -55.58,34.4 -72.28,66.09 -8.43,16 -22.91,23.02 -27.97,8.05C153.44,141.43 125.2,48.96 105.17,23.22 85.56,-1.97 77.8,0.26 77.8,0.26Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="0.41878"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/ic_splash_logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -11,7 +12,9 @@
|
|||||||
android:id="@+id/video_view"
|
android:id="@+id/video_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center"
|
||||||
|
app:fastforward_increment="10000"
|
||||||
|
app:rewind_increment="10000" />
|
||||||
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
<!-- app:controller_layout_id="@layout/player_custom_control"/>-->
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.ProgressIndicator
|
<com.google.android.material.progressindicator.ProgressIndicator
|
||||||
|
@ -81,6 +81,91 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="#ffffff"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="7dp"
|
||||||
|
android:paddingEnd="7dp"
|
||||||
|
android:text="@string/settings"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linear_settings_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/account"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_baseline_subtitles_24" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_secondary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_secondary"
|
||||||
|
android:textColor="@android:color/primary_text_light"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_settings_secondary_desc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/settings_secondary_desc"
|
||||||
|
android:textColor="@android:color/secondary_text_light" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_secondary"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/linear_info"
|
android:id="@+id/linear_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -157,10 +242,11 @@
|
|||||||
android:layout_margin="7dp"
|
android:layout_margin="7dp"
|
||||||
android:paddingStart="48dp"
|
android:paddingStart="48dp"
|
||||||
android:paddingEnd="48dp"
|
android:paddingEnd="48dp"
|
||||||
android:text="Licenses"
|
android:text="@string/licenses"
|
||||||
android:textColor="@android:color/primary_text_light"
|
android:textColor="@android:color/primary_text_light"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
@ -19,10 +19,11 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView"
|
android:id="@+id/text_my_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
@ -42,20 +43,34 @@
|
|||||||
tools:listitem="@layout/item_media" />
|
tools:listitem="@layout/item_media" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="7dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_new_episodes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingTop="15dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:text="@string/new_episodes"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_new_episodes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_media" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_home"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 989 B |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<!-- home fragment -->
|
<!-- home fragment -->
|
||||||
<string name="my_list">Meine Liste</string>
|
<string name="my_list">Meine Liste</string>
|
||||||
|
<string name="new_episodes">Neue Episoden</string>
|
||||||
|
|
||||||
<!-- search fragment -->
|
<!-- search fragment -->
|
||||||
<string name="search_hint">Suche nach Filmen und Serien</string>
|
<string name="search_hint">Suche nach Filmen und Serien</string>
|
||||||
@ -16,6 +17,7 @@
|
|||||||
<string name="text_episodes_count">%1$d Episoden</string>
|
<string name="text_episodes_count">%1$d Episoden</string>
|
||||||
<string name="text_runtime">%1$d Minuten</string>
|
<string name="text_runtime">%1$d Minuten</string>
|
||||||
<string name="component_episode_title">Episode %1$d %2$s</string>
|
<string name="component_episode_title">Episode %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Episode %1$d %2$s (OmU)</string>
|
||||||
|
|
||||||
<!-- settings fragment -->
|
<!-- settings fragment -->
|
||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
@ -24,6 +26,9 @@
|
|||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
<string name="licenses">Lizenzen</string>
|
<string name="licenses">Lizenzen</string>
|
||||||
|
<string name="settings">Einstellungen</string>
|
||||||
|
<string name="settings_secondary">Bevorzuge alternativen Stream</string>
|
||||||
|
<string name="settings_secondary_desc">Untertitle-Stream verwenden, sofern vorhanden</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">speichern</string>
|
<string name="save">speichern</string>
|
||||||
|
@ -3,4 +3,6 @@
|
|||||||
<color name="colorPrimary">#6200EE</color>
|
<color name="colorPrimary">#6200EE</color>
|
||||||
<color name="colorPrimaryDark">#3700B3</color>
|
<color name="colorPrimaryDark">#3700B3</color>
|
||||||
<color name="colorAccent">#03DAC5</color>
|
<color name="colorAccent">#03DAC5</color>
|
||||||
|
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
</resources>
|
</resources>
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<!-- home fragment -->
|
<!-- home fragment -->
|
||||||
<string name="my_list">My list</string>
|
<string name="my_list">My list</string>
|
||||||
|
<string name="new_episodes">New episodes</string>
|
||||||
|
|
||||||
<!-- search fragment -->
|
<!-- search fragment -->
|
||||||
<string name="search_hint">Search for movies and series</string>
|
<string name="search_hint">Search for movies and series</string>
|
||||||
@ -21,6 +22,7 @@
|
|||||||
<string name="text_episodes_count">%1$d episodes</string>
|
<string name="text_episodes_count">%1$d episodes</string>
|
||||||
<string name="text_runtime">%1$d Minutes</string>
|
<string name="text_runtime">%1$d Minutes</string>
|
||||||
<string name="component_episode_title">Episode %1$d %2$s</string>
|
<string name="component_episode_title">Episode %1$d %2$s</string>
|
||||||
|
<string name="component_episode_title_sub">Episode %1$d %2$s (Sub)</string>
|
||||||
<string name="component_poster_desc" translatable="false">episode poster</string>
|
<string name="component_poster_desc" translatable="false">episode poster</string>
|
||||||
<string name="component_watched_desc" translatable="false">already watched</string>
|
<string name="component_watched_desc" translatable="false">already watched</string>
|
||||||
|
|
||||||
@ -33,6 +35,9 @@
|
|||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
<string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
<string name="licenses">Licenses</string>
|
<string name="licenses">Licenses</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="settings_secondary">Prefer secondary (sub) stream</string>
|
||||||
|
<string name="settings_secondary_desc">Use the subtitles stream if present</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">save</string>
|
||||||
@ -46,8 +51,10 @@
|
|||||||
|
|
||||||
<!-- save keys -->
|
<!-- save keys -->
|
||||||
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
|
<string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
|
||||||
|
<string name="preference_file_key" translatable="false">org.mosad.teapod.preferences</string>
|
||||||
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
|
<string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
|
||||||
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
<string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
|
||||||
|
<string name="save_key_prefer_secondary" translatable="false">org.mosad.teapod.prefer_secondary</string>
|
||||||
|
|
||||||
<!-- intents & states -->
|
<!-- intents & states -->
|
||||||
<string name="intent_stream_url" translatable="false">intent_stream_url</string>
|
<string name="intent_stream_url" translatable="false">intent_stream_url</string>
|
||||||
|
@ -7,13 +7,17 @@
|
|||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.MaterialComponents.Light.NoActionBar.FullScreen" parent="@style/Theme.MaterialComponents.Light.NoActionBar">
|
<style name="PlayerTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
<item name="android:windowContentOverlay">@null</item>
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="android:windowBackground">@drawable/bg_splash</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- shapes -->
|
<!-- shapes -->
|
||||||
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
<style name="ShapeAppearance.Teapod.RoundedPoster" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
|
342
etc/drawable_resources/icon.svg
Normal file
After Width: | Height: | Size: 548 KiB |
1
fastlane/metadata/android/de-DE/changelogs/1000.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Version 0.1 ist der erste öffentliche Release von Teapod.
|
11
fastlane/metadata/android/de-DE/full_description.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Teapod ist eine inoffizielle App für Anime-on-Demand (AoD).
|
||||||
|
|
||||||
|
* Schau dir alle Title von AoD auf deinem Android Gerät an
|
||||||
|
* Nativer Player auf Basis des ExoPayers
|
||||||
|
* Bevorzuge die OmU Version über die App-Einstellungen
|
||||||
|
* Speicher deine lieblings Anime in "Meine Liste"
|
||||||
|
|
||||||
|
Zur Verfügung gestellt unter den Bedingungen der GNU GPL 3 oder höher.
|
||||||
|
Dieses Projekt ist in keiner Weise mit Anime-on-Demand verbunden.
|
||||||
|
|
||||||
|
Bitte melde Fehler und Probleme an support@mosad.xyz
|
After Width: | Height: | Size: 848 KiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 572 KiB |
After Width: | Height: | Size: 1.2 MiB |
1
fastlane/metadata/android/de-DE/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Android App für AoD
|
1
fastlane/metadata/android/de-DE/title.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Teapod
|
11
fastlane/metadata/android/en-US/full_description.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Teapod is a unoffical App for Anime-on-Demand (AoD).
|
||||||
|
|
||||||
|
* Watch all animes from AoD on your Android device
|
||||||
|
* Native Player based on ExoPayer
|
||||||
|
* Prefer the OmU version via the app settings
|
||||||
|
* Save your favorite animes to "My List"
|
||||||
|
|
||||||
|
Licensed under the terms and conditions of GPL 3.
|
||||||
|
This Project is not associated with Anime-on-Demand in any way.
|
||||||
|
|
||||||
|
Please report bugs and issues to support@mosad.xyz
|
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Android App for AoD
|