├── .env
├── app
├── .gitignore
├── google-services.json.enc
├── sampledata
│ └── images
│ │ ├── img_1.jpg
│ │ ├── img_2.jpg
│ │ ├── img_3.png
│ │ ├── img_4.jpg
│ │ └── img_5.png
├── src
│ ├── main
│ │ ├── ic_launcher-web.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values
│ │ │ │ ├── integers.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── strings_leakcanary.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── values-v19
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── values-w600dp
│ │ │ │ ├── integers.xml
│ │ │ │ └── dimens.xml
│ │ │ ├── values-w900dp
│ │ │ │ └── integers.xml
│ │ │ ├── drawable
│ │ │ │ ├── img_error.xml
│ │ │ │ ├── ic_skip_next_black_24dp.xml
│ │ │ │ ├── ic_cast_black_24dp.xml
│ │ │ │ ├── ic_search_black_24dp.xml
│ │ │ │ ├── img_loading.xml
│ │ │ │ ├── ic_history_black_24dp.xml
│ │ │ │ ├── ic_cast_connected_black_24dp.xml
│ │ │ │ └── ic_settings_black_24dp.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── fragment_episode.xml
│ │ │ │ ├── activity_onboarding.xml
│ │ │ │ ├── item_unknown.xml
│ │ │ │ ├── activity_home.xml
│ │ │ │ ├── item_empty.xml
│ │ │ │ ├── fragment_search.xml
│ │ │ │ └── item_episode.xml
│ │ │ └── menu
│ │ │ │ └── home.xml
│ │ ├── kotlin
│ │ │ └── brunodles
│ │ │ │ ├── animewatcher
│ │ │ │ ├── cast
│ │ │ │ │ ├── DeviceConnectedListener.kt
│ │ │ │ │ ├── Caster.kt
│ │ │ │ │ ├── CastOptionsProvider.kt
│ │ │ │ │ ├── MultiCaster.kt
│ │ │ │ │ └── GoogleCaster.kt
│ │ │ │ ├── persistence
│ │ │ │ │ ├── UrlFixer.kt
│ │ │ │ │ └── Preferences.kt
│ │ │ │ ├── search
│ │ │ │ │ ├── AutoCompleteAdapter.kt
│ │ │ │ │ └── SearchController.kt
│ │ │ │ ├── parcelable
│ │ │ │ │ ├── EpisodeParceler.kt
│ │ │ │ │ └── EpisodeParcel.kt
│ │ │ │ ├── Application.kt
│ │ │ │ ├── explorer
│ │ │ │ │ └── Episode.kt
│ │ │ │ ├── api
│ │ │ │ │ └── ApiFactory.kt
│ │ │ │ ├── player
│ │ │ │ │ ├── PlayerListener.kt
│ │ │ │ │ ├── Player.kt
│ │ │ │ │ └── EpisodeController.kt
│ │ │ │ ├── home
│ │ │ │ │ └── HomeActivity.kt
│ │ │ │ ├── nextepisodes
│ │ │ │ │ ├── EpisodeAdapter.kt
│ │ │ │ │ └── NextEpisodeFragment.kt
│ │ │ │ └── ImageLoader.kt
│ │ │ │ ├── extensions
│ │ │ │ └── ListExtensions.kt
│ │ │ │ ├── rxpicasso
│ │ │ │ └── RxPicasso.kt
│ │ │ │ ├── bindingadapter
│ │ │ │ └── RecyclerViewBindingAdapter.kt
│ │ │ │ ├── adapter
│ │ │ │ ├── EmptyAdapter.kt
│ │ │ │ └── ViewDataBindingAdapter.kt
│ │ │ │ ├── rxfirebase
│ │ │ │ ├── SingleValueEvent.kt
│ │ │ │ ├── TypedEvent.kt
│ │ │ │ └── ChildAddedEvent.kt
│ │ │ │ ├── components
│ │ │ │ └── TextInputAutoCompleteTextView.kt
│ │ │ │ └── collection
│ │ │ │ └── ArrayWithKeys.kt
│ │ └── manifest
│ │ │ └── AndroidManifest.xml
│ ├── debug
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── brunodles
│ │ │ └── animewatcher
│ │ │ ├── AnrWatchDogMod.kt
│ │ │ ├── StrictModeMod.kt
│ │ │ └── LocalCrashes.kt
│ ├── connectsdk_enabled
│ │ └── kotlin
│ │ │ └── brunodles
│ │ │ └── animewatcher
│ │ │ └── cast
│ │ │ └── ConnectSdkCasterFactory.kt
│ ├── androidTest
│ │ └── java
│ │ │ └── brunodles
│ │ │ └── animewatcher
│ │ │ └── ExampleInstrumentedTest.kt
│ └── connectsdk_disabled
│ │ └── kotlin
│ │ └── brunodles
│ │ └── animewatcher
│ │ └── cast
│ │ └── ConnectSdkCasterFactory.kt
├── launchOnDevice.sh
├── README.md
└── proguard-rules.pro
├── server-ktor
├── .gitignore
├── Procfile
├── src
│ ├── main
│ │ └── java
│ │ │ └── com
│ │ │ └── brunodles
│ │ │ └── animewatcher
│ │ │ └── serverktor
│ │ │ ├── Main.kt
│ │ │ └── Application.kt
│ └── test
│ │ └── kotlin
│ │ └── com
│ │ └── brunodles
│ │ └── animewatcher
│ │ └── serverktor
│ │ └── ApplicationKtTest.kt
└── build.gradle
├── cli
├── .gitignore
├── README.md
├── src
│ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── brunodles
│ │ └── animewatcher
│ │ └── cli
│ │ └── Decoder.kt
└── build.gradle
├── explorer
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ ├── cache
│ │ │ └── responses
│ │ │ │ ├── animakai
│ │ │ │ ├── error_episodes.json
│ │ │ │ ├── umaruchan_episodes.json
│ │ │ │ └── tskipro_episodes.json
│ │ │ │ ├── animesorion
│ │ │ │ ├── noNext_episodes.json
│ │ │ │ └── singleNext_episodes.json
│ │ │ │ ├── tvcurse
│ │ │ │ ├── player_without_next_episodes.json
│ │ │ │ └── player_with_next_episodes.json
│ │ │ │ ├── anitubebr
│ │ │ │ └── player_episodes.json
│ │ │ │ ├── animetubebrasil
│ │ │ │ └── player_episodes.json
│ │ │ │ ├── onepiecex
│ │ │ │ └── player_episodes.json
│ │ │ │ ├── anitubesite
│ │ │ │ └── player_episodes.json
│ │ │ │ └── animesonlinebr
│ │ │ │ └── player_episodes.json
│ │ └── kotlin
│ │ │ ├── brunodles
│ │ │ ├── animewatcher
│ │ │ │ ├── testhelper
│ │ │ │ │ ├── FailFetcher.kt
│ │ │ │ │ ├── RetryFetcher.kt
│ │ │ │ │ └── ResourceFetcher.kt
│ │ │ │ └── decoders
│ │ │ │ │ ├── AnitubeSiteFactoryTest.kt
│ │ │ │ │ ├── AnimeTubeBrasilFactoryTest.kt
│ │ │ │ │ ├── OnePieceXFactoryTest.kt
│ │ │ │ │ ├── AnitubeBrFactoryTest.kt
│ │ │ │ │ ├── AnimesOnlineBrFactoryTest.kt
│ │ │ │ │ ├── XvideosFactoryTest.kt
│ │ │ │ │ ├── TvCurseFactoryTest.kt
│ │ │ │ │ ├── AnimaKaiFactoryTest.kt
│ │ │ │ │ └── AnimesOrionFactoryTest.kt
│ │ │ ├── ResourceLoader.kt
│ │ │ └── urlfetcher
│ │ │ │ └── CacheFetcherTest.kt
│ │ │ └── JsonMaker.kt
│ └── main
│ │ └── kotlin
│ │ └── brunodles
│ │ ├── animewatcher
│ │ ├── decoders
│ │ │ ├── TypeAlias.kt
│ │ │ ├── UrlChecker.kt
│ │ │ ├── AnimeTubeBrasilFactory.kt
│ │ │ ├── AnimaKaiFactory.kt
│ │ │ ├── AnitubeBrFactory.kt
│ │ │ ├── AnitubeSiteFactory.kt
│ │ │ ├── TvCurseFactory.kt
│ │ │ └── AnimesOrionFactory.kt
│ │ ├── explorer
│ │ │ ├── PageParser.kt
│ │ │ └── Episode.kt
│ │ └── AlchemistFactory.kt
│ │ ├── kotlin
│ │ └── annotation
│ │ │ └── NoArgs.kt
│ │ └── urlfetcher
│ │ ├── Logger.kt
│ │ ├── UrlFetcherComposable.kt
│ │ ├── Extensions.kt
│ │ ├── JsoupFetcher.kt
│ │ ├── UrlFetcher.kt
│ │ ├── RedirectFetcher.kt
│ │ └── CacheFetcher.kt
├── README.md
└── build.gradle
├── decoders
├── anitubex
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ │ ├── test
│ │ └── kotlin
│ │ │ └── brunodles
│ │ │ └── anitubex
│ │ │ └── AnitubexFactoryTest.kt
│ │ └── main
│ │ └── kotlin
│ │ └── brunodles
│ │ └── anitubex
│ │ └── AnitubexFactory.kt
├── animesproject
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ │ ├── test
│ │ └── kotlin
│ │ │ └── brunodles
│ │ │ └── animesproject
│ │ │ └── AnimesProjectFactoryTest.kt
│ │ └── main
│ │ └── kotlin
│ │ └── brunodles
│ │ └── animesproject
│ │ └── AnimesProjectFactory.kt
└── README.md
├── feature_toggle.properties
├── keystore
├── server-spark
├── Procfile
├── herokuRequest
├── build.gradle
├── server_blueprint.md
├── src
│ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── brunodles
│ │ └── videowatcher
│ │ └── serverspark
│ │ └── ApplicationRouter.kt
└── README.md
├── cloudfunctions
├── .firebaserc
├── firebase.json
├── functions
│ ├── package.json
│ ├── search.js
│ ├── index.js
│ └── gateway.js
├── .gitignore
└── README.md
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── checkstyle.gradle
├── decoder_build.gradle
├── ktlint.gradle
├── jacoco.gradle
├── sites.gradle
├── resource_helper.gradle
└── buildconfig.gradle
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── kotlinc.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── runConfigurations.xml
├── settings.gradle
├── cache
├── README.md
└── yasopp.onepieceex.com.br
│ └── playerphpcodeplayseuZKaRb4qH18qkwx2FxodvEhf8pFKKbt6
├── .travis.yml
├── ciVerifications
├── gradle.properties
├── .circleci
└── config.yml
├── README.md
├── Firebase.md
└── gradlew.bat
/.env:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/server-ktor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/cli/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | cache
3 |
--------------------------------------------------------------------------------
/explorer/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /cache
--------------------------------------------------------------------------------
/decoders/anitubex/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/feature_toggle.properties:
--------------------------------------------------------------------------------
1 | CONNECT_SDK=true
--------------------------------------------------------------------------------
/decoders/animesproject/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/cache:
--------------------------------------------------------------------------------
1 | ../../../../cache
--------------------------------------------------------------------------------
/keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/keystore
--------------------------------------------------------------------------------
/server-ktor/Procfile:
--------------------------------------------------------------------------------
1 | web: ./server-ktor/build/install/server-ktor/bin/server-ktor
--------------------------------------------------------------------------------
/server-spark/Procfile:
--------------------------------------------------------------------------------
1 | web: ./server-spark/build/install/server-spark/bin/server-spark
--------------------------------------------------------------------------------
/decoders/anitubex/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: rootProject.file("gradle/decoder_build.gradle")
--------------------------------------------------------------------------------
/decoders/README.md:
--------------------------------------------------------------------------------
1 | This module will be removed, but still have some code that need to be moved.
--------------------------------------------------------------------------------
/decoders/animesproject/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: rootProject.file("gradle/decoder_build.gradle")
--------------------------------------------------------------------------------
/cloudfunctions/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "animewatcher-bbdf0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/app/google-services.json.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/google-services.json.enc
--------------------------------------------------------------------------------
/app/sampledata/images/img_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/sampledata/images/img_1.jpg
--------------------------------------------------------------------------------
/app/sampledata/images/img_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/sampledata/images/img_2.jpg
--------------------------------------------------------------------------------
/app/sampledata/images/img_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/sampledata/images/img_3.png
--------------------------------------------------------------------------------
/app/sampledata/images/img_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/sampledata/images/img_4.jpg
--------------------------------------------------------------------------------
/app/sampledata/images/img_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/sampledata/images/img_5.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/explorer/README.md:
--------------------------------------------------------------------------------
1 | # Anime Watcher - Explorer
2 | Explores the html pages for wanted content.
3 |
4 | This module exposes the way to decode the pages.
--------------------------------------------------------------------------------
/app/src/main/res/values/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1
4 |
--------------------------------------------------------------------------------
/cloudfunctions/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "predeploy": [
4 | "npm --prefix \"$RESOURCE_DIR\" run lint"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v19/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 56dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 2
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w900dp/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 3
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 120dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/TypeAlias.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | typealias Regexp = com.brunodles.alchemist.regex.Regex
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brunodles/anime-watcher/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/cast/DeviceConnectedListener.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | internal typealias DeviceConnectedListener = (Caster) -> Unit
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # Anime Watcher - CLI
2 | This Command Line Interface just interacts with explorer module to decode pages
3 | locally.
4 |
5 | We may add more functions here later.
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_leakcanary.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AnimeWatcher leaks
5 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/kotlin/annotation/NoArgs.kt:
--------------------------------------------------------------------------------
1 | package brunodles.kotlin.annotation
2 |
3 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
4 | @Retention(AnnotationRetention.BINARY)
5 | annotation class NoArgs
6 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/explorer/PageParser.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.explorer
2 |
3 | interface PageParser {
4 |
5 | fun isEpisode(url: String): Boolean
6 |
7 | fun episode(url: String): Episode
8 | }
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/persistence/UrlFixer.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.persistence
2 |
3 | private val INVALID_TEXT_PATTERN = Regex("[^\\d\\w]+")
4 |
5 | fun fixUrlToFirebase(url: String): String = url.replace(INVALID_TEXT_PATTERN, "")
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #f49569
4 | #c65e35
5 | #e90111
6 |
7 |
--------------------------------------------------------------------------------
/app/launchOnDevice.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [ -z $1 ]; then
3 | echo "Missing url parameter"
4 | echo "Usage: $0 "
5 | echo "Sample: $0 https://github.com/brunodles/anime-watcher"
6 | else
7 | adb shell am start -a "android.intent.action.VIEW" -d "$1"
8 | fi
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 16 18:39:37 BRT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/gradle/checkstyle.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'checkstyle'
2 |
3 | checkstyle {
4 | toolVersion "7.6.1"
5 | sourceSets = [ project.sourceSets.main ]
6 | }
7 |
8 | checkstyleMain {
9 | source 'src/main/'
10 | exclude '**/build/**'
11 | configFile rootProject.file('checkstyle.xml')
12 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/Logger.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | internal object Logger {
4 |
5 | var useLog: Boolean = false
6 |
7 | internal fun log(function: () -> String) {
8 | if (useLog)
9 | println("UrlFetcher: ${function()}")
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-v19/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':explorer'
2 | include ':cli'
3 | include ':server-spark', ':server-ktor'
4 |
5 | // include android related modules inside this verification
6 | // This will be used to skip android modules for web server releases.
7 | if ((System.getenv("INCLUDE_ANDROID") ?: "true").toBoolean()) {
8 | include ':app'
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4dp
4 | 8dp
5 | 2dp
6 | 80dp
7 |
8 | 0dp
9 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animakai/error_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": " online aqui no site",
3 | "number": 1,
4 | "animeName": " online aqui no site",
5 | "image": "http/imagens/848x380/",
6 | "video": "",
7 | "link": "https://www.animeskai.net/anime/my-hero/ep-01",
8 | "nextEpisodes": [],
9 | "temporaryVideoUrl": false
10 | }
--------------------------------------------------------------------------------
/gradle/decoder_build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 | apply plugin: 'kotlin'
3 |
4 | dependencies {
5 | compile project(':explorer')
6 |
7 | testCompile project(':decodertesthelper')
8 | testCompile 'junit:junit:4.12'
9 | testCompile 'com.greghaskins:spectrum:1.1.1'
10 | }
11 |
12 | sourceCompatibility = "1.7"
13 | targetCompatibility = "1.7"
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/extensions/ListExtensions.kt:
--------------------------------------------------------------------------------
1 | package brunodles.extensions
2 |
3 | import java.util.Random
4 |
5 | private fun List.firsts(max: Int): List {
6 | val lastIndex = if (this.size >= max) max else this.size
7 | return this.subList(0, lastIndex)
8 | }
9 |
10 | private fun List.random(): E = this[Random().nextInt(this.size)]
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/search/AutoCompleteAdapter.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.search
2 |
3 | import android.content.Context
4 | import android.widget.ArrayAdapter
5 | import android.widget.Filterable
6 |
7 | class AutoCompleteAdapter(context: Context) :
8 | ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line), Filterable {
9 |
10 | }
--------------------------------------------------------------------------------
/cache/README.md:
--------------------------------------------------------------------------------
1 | # Cache
2 | This folder provides cached pages for our tests.
3 | With it we don't need to fetch pages every time.
4 |
5 | The cache is also useful to look the real page content, as it will not execute
6 | `javascript` nor `iframes`. Some pages also have different responses depending
7 | on `user agent` used to fetch the page.
8 |
9 | This cache cam be disabled during build time.
10 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animesorion/noNext_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Himouto! Umaru-chan Episódio 12 – FINAL",
3 | "number": 12,
4 | "animeName": "Himouto! Umaru-chan 1 Legendado",
5 | "video": "https://www.blogger.com/video-play.mp4?contentId\u003de4e3e680e0b6ed74",
6 | "link": "http://www.animesorion.tv/51555",
7 | "nextEpisodes": [],
8 | "temporaryVideoUrl": false
9 | }
--------------------------------------------------------------------------------
/server-spark/herokuRequest:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [ -z $1 ]; then
3 | echo "Missing url parameter"
4 | echo "Usage: $0 "
5 | echo "Sample: $0 https://github.com/brunodles/anime-watcher"
6 | else
7 | # deprecated
8 | #curl https://anime-watcher-spark.herokuapp.com/decoder -d "$1"
9 | curl -G https://anime-watcher-spark.herokuapp.com/v1/decoder --data-urlencode "url=$1"
10 | fi
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_episode.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_skip_next_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/cache/yasopp.onepieceex.com.br/playerphpcodeplayseuZKaRb4qH18qkwx2FxodvEhf8pFKKbt6:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {"total":2,"LQ":"https:\/\/yasopp.onepieceex.com.br\/episodios\/lq\/OpEx_210_LQ.mp4?st=faAdmibhdMuL59gREzBICg&e=1540950612","Online":"https:\/\/yasopp.onepieceex.com.br\/episodios\/online\/OpEx_210_online.webm?st=naV6WzqRzANboDn3miGwyw&e=1540950612","andamento":100,"code":0,"mensagem":"Tudo pronto."}
5 |
6 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animakai/umaruchan_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Himouto! Umaru-chan 1 online aqui no site",
3 | "number": 1,
4 | "animeName": "Himouto! Umaru-chan online aqui no site",
5 | "image": "http/imagens/848x380/",
6 | "video": "http://www.blogger.com/video-play.mp4?contentId\u003da1bc047412d3b897",
7 | "link": "https://www.animeskai.net/himouto-umaru-chan/ep-1",
8 | "nextEpisodes": [],
9 | "temporaryVideoUrl": false
10 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/rxpicasso/RxPicasso.kt:
--------------------------------------------------------------------------------
1 | package brunodles.rxpicasso
2 |
3 | import android.graphics.Bitmap
4 | import com.squareup.picasso.RequestCreator
5 | import io.reactivex.Single
6 |
7 | fun RequestCreator.asSingle(): Single {
8 | return Single.create { subject ->
9 | try {
10 | subject.onSuccess(this.get())
11 | } catch (exception: Exception) {
12 | subject.onError(exception)
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/gradle/ktlint.gradle:
--------------------------------------------------------------------------------
1 | configurations {
2 | ktlint
3 | }
4 |
5 | dependencies {
6 | ktlint 'com.github.shyiko:ktlint:0.14.0'
7 | }
8 |
9 | task ktlint(type: JavaExec) {
10 | group "verification"
11 | description "Check kotlin code style."
12 | classpath configurations.ktlint
13 | main 'com.github.shyiko.ktlint.Main'
14 | args "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml", "src/**/*.kt"
15 | }
16 |
17 | check.dependsOn ktlint
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/testhelper/FailFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.testhelper
2 |
3 | import brunodles.urlfetcher.UrlFetcher
4 | import org.jsoup.nodes.Document
5 |
6 | internal class FailFetcher : UrlFetcher {
7 |
8 | override fun get(url: String): Document {
9 | throw FetchingException("Failed to fetch: $url")
10 | }
11 |
12 | class FetchingException internal constructor(msg: String) : RuntimeException(msg)
13 | }
14 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/tvcurse/player_without_next_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Assistir Himouto! Umaru-chan ova 11 – Especial ova 11 Online",
3 | "number": 11,
4 | "animeName": "Himouto! Umaru-chan",
5 | "image": "https://tvcurse.com/imgs/himouto-umaru-chan-ova-11.webp",
6 | "video": "https://tvcurse.com/dd/YzRjMjhmMDc4MGY5NzExNg\u003d\u003d",
7 | "link": "https://tvcurse.com/?p\u003d43449",
8 | "nextEpisodes": [],
9 | "temporaryVideoUrl": false
10 | }
--------------------------------------------------------------------------------
/app/src/debug/kotlin/brunodles/animewatcher/AnrWatchDogMod.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import com.brunodles.environmentmods.annotation.ModFor
4 | import com.github.anrwatchdog.ANRWatchDog
5 |
6 | object AnrWatchDogMod {
7 |
8 | private val TIMEOUT = 10000
9 |
10 | @JvmStatic
11 | @ModFor(Application::class)
12 | fun startAnrWatchDog(){
13 | ANRWatchDog(TIMEOUT)
14 | .setIgnoreDebugger(true)
15 | .start()
16 | }
17 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk: oraclejdk8
3 |
4 | env:
5 | - INCLUDE_ANDROID=false
6 |
7 | script:
8 | - ./gradlew clean assemble
9 | - ./gradlew ktlint checkstyleMain --continue
10 | # - ./gradlew cleanTest test -Pusecache=true -PparallelForks=1
11 |
12 | before_cache:
13 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
14 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
15 | cache:
16 | directories:
17 | - $HOME/.gradle/caches/
18 | - $HOME/.gradle/wrapper/
19 |
--------------------------------------------------------------------------------
/app/src/connectsdk_enabled/kotlin/brunodles/animewatcher/cast/ConnectSdkCasterFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | import android.app.Activity
4 | import android.widget.ImageButton
5 |
6 | object ConnectSdkCasterFactory {
7 | internal fun create(
8 | activity: Activity, mediaRouteButton: ImageButton?,
9 | listener: DeviceConnectedListener? = null
10 | ): ConnectSdkCaster {
11 | return ConnectSdkCaster(activity, mediaRouteButton, listener)
12 | }
13 | }
--------------------------------------------------------------------------------
/server-ktor/src/main/java/com/brunodles/animewatcher/serverktor/Main.kt:
--------------------------------------------------------------------------------
1 | package com.brunodles.animewatcher.serverktor
2 |
3 | import io.ktor.server.engine.embeddedServer
4 | import io.ktor.server.netty.Netty
5 |
6 | fun main(args: Array) {
7 | val port = getHerokuAssignedPort()
8 | val server = embeddedServer(Netty, port = port) {
9 | animewatcher()
10 | }
11 | server.start(wait = true)
12 | }
13 |
14 | fun getHerokuAssignedPort(): Int = System.getenv("PORT")?.toInt() ?: 4567
15 |
--------------------------------------------------------------------------------
/cli/src/main/kotlin/com/brunodles/animewatcher/cli/Decoder.kt:
--------------------------------------------------------------------------------
1 | package com.brunodles.animewatcher.cli
2 |
3 | import brunodles.animewatcher.decoders.UrlChecker
4 | import brunodles.urlfetcher.UrlFetcher
5 |
6 | fun main(args: Array) {
7 | val cacheDir = UrlFetcher.cacheDir + "/cli/cache"
8 | println("CacheDir $cacheDir")
9 | UrlFetcher.cacheDir = cacheDir
10 | UrlFetcher.useLog = true
11 | println("\n\n")
12 | val videoInfo = UrlChecker.videoInfo(args[0])
13 | println(videoInfo)
14 | }
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animakai/tskipro_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Tsukipro The Animation 1 online aqui no site",
3 | "number": 1,
4 | "animeName": "Tsukipro The Animation online aqui no site",
5 | "image": "http://www.animekai.info/2017/10/Screenshot_33.jpg",
6 | "video": "http://www.blogger.com/video-play.mp4?contentId\u003db186c220e9973f58",
7 | "link": "https://www.animekaionline.com/tsukipro-the-animation/episodio-1",
8 | "nextEpisodes": [],
9 | "temporaryVideoUrl": false
10 | }
--------------------------------------------------------------------------------
/ciVerifications:
--------------------------------------------------------------------------------
1 | # This script is currently simulating ci builds.
2 | # It does the same commands currently on ci.
3 | # need to be on environment vars:
4 | # GOOGLE_SERVICES_KEY
5 |
6 | # Setup environment
7 | rm app/google-services.json
8 |
9 | # Fail fast, if a command fail the script will fail
10 | set -e
11 | ./gradlew clean app:googleServicesDecrypt assembleDebug
12 | ./gradlew lint ktlint checkstyleMain --continue -x app:ktlint
13 | ./gradlew cleanTest test -Pusecache=true -PparallelForks=1
14 | ./gradlew jacocoTestReport
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/bindingadapter/RecyclerViewBindingAdapter.kt:
--------------------------------------------------------------------------------
1 | package brunodles.bindingadapter
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.v7.widget.RecyclerView
5 |
6 | object RecyclerViewBindingAdapter {
7 |
8 | @JvmStatic
9 | @BindingAdapter("nestedScrollingEnabled", requireAll = false)
10 | fun nestedScrollingEnabled(recyclerView: RecyclerView, nestedScrollingEnabled: Boolean) {
11 | recyclerView.isNestedScrollingEnabled = nestedScrollingEnabled
12 | }
13 | }
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/anitubebr/player_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "One Piece 01",
3 | "number": 1,
4 | "animeName": "One Piece",
5 | "image": "",
6 | "link": "https://anitubebr.com/vd/19249/",
7 | "nextEpisodes": [
8 | {
9 | "description": "Próximo Episódio",
10 | "number": 2,
11 | "animeName": "One Piece",
12 | "link": "https://anitubebr.com/vd/19250/",
13 | "nextEpisodes": [],
14 | "temporaryVideoUrl": false
15 | }
16 | ],
17 | "temporaryVideoUrl": false
18 | }
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/testhelper/RetryFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.testhelper
2 |
3 | import brunodles.urlfetcher.UrlFetcher
4 | import org.jsoup.nodes.Document
5 |
6 | internal class RetryFetcher(private val fetcher: UrlFetcher) : UrlFetcher {
7 |
8 | override fun get(url: String): Document {
9 | return try {
10 | fetcher.get(url)
11 | } catch (e: Exception) {
12 | e.printStackTrace(System.err)
13 | fetcher.get(url)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/debug/kotlin/brunodles/animewatcher/StrictModeMod.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import android.os.StrictMode
4 | import com.brunodles.environmentmods.annotation.ModFor
5 |
6 | object StrictModeMod {
7 |
8 | @JvmStatic
9 | @ModFor(Application::class)
10 | fun applyThreadPolicy() {
11 | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
12 | .detectAll()
13 | .penaltyLog()
14 | .penaltyFlashScreen()
15 | .penaltyDeath()
16 | .build())
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animetubebrasil/player_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Eu Sou Ruffy!",
3 | "number": 1,
4 | "animeName": "One Piece",
5 | "video": "https://www.blogger.com/video-play.mp4?contentId\u003d3eb2b428663ef38f",
6 | "link": "https://animetubebrasil.com/1582/",
7 | "nextEpisodes": [
8 | {
9 | "description": "Next",
10 | "number": 2,
11 | "animeName": "One Piece",
12 | "link": "https://animetubebrasil.com/1583/",
13 | "nextEpisodes": [],
14 | "temporaryVideoUrl": false
15 | }
16 | ],
17 | "temporaryVideoUrl": false
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cast_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/parcelable/EpisodeParceler.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.parcelable
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 |
5 | object EpisodeParceler {
6 |
7 | fun toParcel(episode: Episode) = EpisodeParcel(episode)
8 |
9 | fun fromParcel(episode: EpisodeParcel): Episode = Episode(
10 | episode.description,
11 | episode.number,
12 | episode.animeName,
13 | episode.image,
14 | episode.video,
15 | episode.link,
16 | episode.nextEpisodes.map { fromParcel(it) }.toList()
17 | )
18 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/adapter/EmptyAdapter.kt:
--------------------------------------------------------------------------------
1 | package brunodles.adapter
2 |
3 | import android.support.v7.widget.RecyclerView
4 | import android.view.ViewGroup
5 |
6 | class EmptyAdapter : RecyclerView.Adapter() {
7 |
8 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
9 | }
10 |
11 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
12 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
13 | }
14 |
15 | override fun getItemCount(): Int = 0
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_history_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animesorion/singleNext_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Boku No Hero Academia Episódio 2",
3 | "number": 2,
4 | "animeName": "Boku No Hero Academia 1",
5 | "video": "https://www.blogger.com/video-play.mp4?contentId\u003db0bccad36e2be8e1",
6 | "link": "https://www.animesorion.site/71672",
7 | "nextEpisodes": [
8 | {
9 | "description": "Next",
10 | "number": 3,
11 | "animeName": "Boku No Hero Academia 1",
12 | "link": "https://www.animesorion.org/71808",
13 | "nextEpisodes": [],
14 | "temporaryVideoUrl": false
15 | }
16 | ],
17 | "temporaryVideoUrl": false
18 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/UrlFetcherComposable.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | class UrlFetcherComposable internal constructor() : UrlFetcher {
6 | private var internalFetcher: UrlFetcher = JsoupFetcher()
7 |
8 | fun withCache(): UrlFetcherComposable {
9 | internalFetcher = CacheFetcher(internalFetcher)
10 | return this
11 | }
12 |
13 | fun withJsRedirect(): UrlFetcherComposable {
14 | internalFetcher = RedirectFetcher(internalFetcher)
15 | return this
16 | }
17 |
18 | override fun get(url: String): Document = internalFetcher.get(url)
19 | }
20 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/onepiecex/player_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Os piratas do Foxy!! A Davy Back!",
3 | "number": 208,
4 | "animeName": "One Piece",
5 | "video": "https:\\/\\/\\w+.onepieceex\\.com\\.br\\/episodios\\/\\w+\\/OpEx_\\d+_\\w+.mp4\\?st\u003d.*",
6 | "link": "https://onepiece-ex.com.br/episodios/online/208/",
7 | "nextEpisodes": [
8 | {
9 | "description": "Next",
10 | "number": 209,
11 | "animeName": "One Piece",
12 | "link": "https://onepiece-ex.com.br/episodios/online/209",
13 | "nextEpisodes": [],
14 | "temporaryVideoUrl": false
15 | }
16 | ],
17 | "temporaryVideoUrl": false
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cast_connected_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/cloudfunctions/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "lint": "eslint .",
6 | "serve": "firebase serve --only functions",
7 | "shell": "firebase functions:shell",
8 | "start": "npm run shell",
9 | "deploy": "firebase deploy --only functions",
10 | "logs": "firebase functions:log"
11 | },
12 | "dependencies": {
13 | "firebase-admin": "~6.0.0",
14 | "firebase-functions": "^2.0.3",
15 | "node-rest-client": "^3.1.0"
16 | },
17 | "devDependencies": {
18 | "eslint": "^4.12.0",
19 | "eslint-plugin-promise": "^3.6.0"
20 | },
21 | "private": true
22 | }
23 |
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/anitubesite/player_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Eu Sou Ruffy!",
3 | "number": 1,
4 | "animeName": "One Piece",
5 | "image": "https://www.anitube.site/player/img/Capa-Player-Cine.png",
6 | "video": "https://www.blogger.com/video-play.mp4?contentId\u003d3eb2b428663ef38f",
7 | "link": "https://www.anitube.site/765/",
8 | "nextEpisodes": [
9 | {
10 | "description": "O Grande Espadachim Aparece!",
11 | "number": 2,
12 | "animeName": "One Piece",
13 | "link": "https://www.anitube.site/802/",
14 | "nextEpisodes": [],
15 | "temporaryVideoUrl": false
16 | }
17 | ],
18 | "temporaryVideoUrl": false
19 | }
--------------------------------------------------------------------------------
/gradle/jacoco.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "jacoco"
2 |
3 | jacoco {
4 | toolVersion = "0.7.6.201602180812"
5 | }
6 |
7 | jacocoTestReport {
8 | reports {
9 | xml.enabled true
10 | csv.enabled true
11 | html.enabled true
12 | }
13 | additionalSourceDirs = files(sourceSets.main.allSource.srcDirs)
14 | sourceDirectories = files(sourceSets.main.allSource.srcDirs)
15 | classDirectories = files(sourceSets.main.output)
16 | }
17 |
18 | test {
19 | ignoreFailures = properties.get("ignoreFailures", false)
20 | reports {
21 | junitXml.enabled = true
22 | html.enabled = true
23 | }
24 | jacoco {
25 | append = false
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_onboarding.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/cast/Caster.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | import android.app.Activity
4 | import android.support.v7.app.MediaRouteButton
5 | import android.widget.ImageButton
6 | import brunodles.animewatcher.explorer.Episode
7 |
8 | interface Caster {
9 |
10 | fun playRemote(currentEpisode: Episode, position: Long)
11 | fun isConnected(): Boolean
12 |
13 | object Factory {
14 | fun multiCaster(activity: Activity, mediaRouterButton: MediaRouteButton, imageButton: ImageButton): Caster
15 | = MultiCaster(activity, mediaRouterButton, imageButton)
16 | }
17 | }
18 |
19 | fun Caster?.isConnected(): Boolean = this?.isConnected() ?: false
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/Extensions.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import org.jsoup.nodes.Element
4 | import org.jsoup.select.Elements
5 | import java.net.URLEncoder
6 |
7 | private val UTF8 = "UTF-8"
8 | fun Elements.src() = this.attr("src").trim()
9 | fun Element.src() = this.attr("src").trim()
10 | fun Element.alt() = this.attr("alt")
11 | fun Elements.alt() = this.attr("alt")
12 | fun Elements.href() = this.attr("href")
13 | fun Element.href() = this.attr("href")
14 | fun String.max(max: Int) = this.substring(0, if (length < max) length else max)
15 | fun String.encodeUTF8() = URLEncoder.encode(this, UTF8)
16 | fun Element.content() = this.attr("content")
17 | fun Elements.content() = this.attr("content")
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/animesonlinebr/player_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Episódio 01 – Eu sou Uzumaki Boruto!! online, Boruto - Episódio 01 – Eu sou Uzumaki Boruto!! Online",
3 | "number": 1,
4 | "animeName": "Boruto",
5 | "video": "https://www.blogger.com/video-play.mp4?contentId\u003d3dbfcd746b2b8f21",
6 | "link": "https://www.animesonlinebr.com.br/video/50034",
7 | "nextEpisodes": [
8 | {
9 | "description": "Boruto - Episódio 02 – O Filho do Hokage!!",
10 | "number": 2,
11 | "animeName": "Boruto",
12 | "link": "https://animesonlinebr.com.br/video/50035",
13 | "nextEpisodes": [],
14 | "temporaryVideoUrl": false
15 | }
16 | ],
17 | "temporaryVideoUrl": false
18 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/JsoupFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import org.jsoup.Jsoup
4 | import org.jsoup.nodes.Document
5 |
6 | internal class JsoupFetcher : UrlFetcher {
7 |
8 | override fun get(url: String): Document = jsoupConnection(url).get()
9 |
10 | private fun jsoupConnection(url: String) = Jsoup.connect(url)
11 | .userAgent(USER_AGENT)
12 | .referrer(REFERRER)
13 | .timeout(10000)
14 | .followRedirects(true)
15 | .ignoreHttpErrors(true)
16 |
17 | companion object {
18 | private val USER_AGENT =
19 | "Mozilla/5.0 (Windows; U; WindowsNT 5.1; en-US; rv1.8.1.6) Gecko/20070725 Firefox/2.0.0.6"
20 | private val REFERRER = "http://www.google.com"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/brunodles/animewatcher/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumentation test, which will execute on an Android device.
13 | *
14 | * @see [Testing documentation](http://d.android.com/tools/testing)
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | @Throws(Exception::class)
20 | fun useAppContext() {
21 | // Context of the app under test.
22 | val appContext = InstrumentationRegistry.getTargetContext()
23 | assertEquals("brunodles.animewatcher", appContext.packageName)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/ResourceLoader.kt:
--------------------------------------------------------------------------------
1 | package brunodles
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import com.google.gson.Gson
5 | import kotlin.reflect.KClass
6 |
7 | private val gson = Gson()
8 |
9 | fun loadResource(path: String): String {
10 | return ClassLoader.getSystemClassLoader()
11 | .getResourceAsStream(
12 | if (path.startsWith('/'))
13 | path.substring(1)
14 | else path
15 | )
16 | .reader()
17 | .readText()
18 | }
19 |
20 | fun loadJsonResource(path: String, type: Class): T =
21 | gson.fromJson(loadResource(path), type)
22 |
23 | fun String.loadJsonResource(type: KClass): T =
24 | loadJsonResource(this, type.java)
25 |
26 | fun String.loadEpisodeResource() =
27 | loadJsonResource(this, Episode::class.java)
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx2048M
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | org.gradle.parallel=true
18 | org.gradle.configureondemand=false
19 |
20 | kotlin.incremental=true
--------------------------------------------------------------------------------
/server-spark/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 | apply plugin: 'application'
3 |
4 | mainClassName = "com.brunodles.videowatcher.serverspark.ApplicationRouterKt"
5 |
6 | jar {
7 | manifest {
8 | attributes 'Main-Class': mainClassName
9 | }
10 |
11 | from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
12 | }
13 |
14 | dependencies {
15 | implementation fileTree(dir: 'libs', include: ['*.jar'])
16 |
17 | implementation project(":explorer")
18 |
19 | implementation "com.sparkjava:spark-kotlin:1.0.0-alpha"
20 | implementation 'com.google.code.gson:gson:2.8.2'
21 |
22 | testImplementation 'junit:junit:4.12'
23 | testImplementation 'com.squareup.okhttp3:okhttp:3.11.0'
24 | }
25 |
26 | task stage {
27 | dependsOn installDist
28 | }
29 |
30 | sourceCompatibility = "1.7"
31 | targetCompatibility = "1.7"
32 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/UrlChecker.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import brunodles.animewatcher.explorer.PageParser
5 |
6 | object UrlChecker {
7 |
8 | private val factories = ArrayList()
9 |
10 | init {
11 | factories.add(AnimaKaiFactory)
12 | factories.add(AnimesOnlineBrFactory)
13 | factories.add(AnimesOrionFactory)
14 | factories.add(AnimeTubeBrasilFactory)
15 | factories.add(AnitubeBrFactory)
16 | factories.add(AnitubeSiteFactory)
17 | factories.add(OnePieceXFactory) // this factory is causing stack overflow
18 | factories.add(TvCurseFactory)
19 | factories.add(XvideosFactory)
20 | }
21 |
22 | fun videoInfo(url: String): Episode? = factories.firstOrNull { it.isEpisode(url) }?.episode(url)
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/connectsdk_disabled/kotlin/brunodles/animewatcher/cast/ConnectSdkCasterFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.castfalse
2 |
3 | import android.app.Activity
4 | import android.widget.ImageButton
5 | import brunodles.animewatcher.explorer.Episode
6 |
7 | object ConnectSdkCasterFactory {
8 | internal fun create(
9 | activity: Activity, mediaRouteButton: ImageButton?,
10 | listener: DeviceConnectedListener? = null
11 | ): Caster {
12 | return object : Caster {
13 | override fun playRemote(currentEpisode: Episode, position: Long) {
14 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
15 | }
16 |
17 | override fun isConnected(): Boolean {
18 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AnimeWatcher
3 |
4 | Cast to Device
5 | ChromeCast
6 | Others
7 | Play on Remote
8 | Next Episodes
9 |
10 | Search Text
11 | Search
12 |
13 | History
14 | Next Episodes
15 | Search
16 | Settings
17 |
18 | The history is empty. Go watch something!
19 | =( Unknown Item
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/cast/CastOptionsProvider.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | import android.content.Context
4 | import com.google.android.gms.cast.CastMediaControlIntent
5 | import com.google.android.gms.cast.framework.CastOptions
6 | import com.google.android.gms.cast.framework.OptionsProvider
7 | import com.google.android.gms.cast.framework.SessionProvider
8 |
9 | @Suppress("unused") // This class is used on `AndroidManifest.xml`
10 | internal class CastOptionsProvider : OptionsProvider {
11 |
12 | override fun getCastOptions(appContext: Context): CastOptions? {
13 | val castOptions = CastOptions.Builder()
14 | .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
15 | .build()
16 | return castOptions
17 | }
18 |
19 | override fun getAdditionalSessionProviders(context: Context): List? {
20 | return null
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_unknown.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
24 |
25 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnitubeSiteFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import org.junit.runner.RunWith
9 | import resource_helper.Resources
10 |
11 | @RunWith(Spectrum::class)
12 | class AnitubeSiteFactoryTest {
13 |
14 | companion object {
15 | val VALID_URLS = arrayOf("https://www.anitube.site/765/")
16 | val INVALID_URLS = arrayOf("anitub")
17 | }
18 |
19 | init {
20 | FactoryChecker.describe(AnitubeSiteFactory) {
21 | whenEpisode(Resources.responses.anitubesite.playerEpisodesJson.loadEpisodeResource())
22 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/Application.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import android.support.multidex.MultiDexApplication
4 | import com.brunodles.environmentmods.annotation.Moddable
5 | import com.crashlytics.android.Crashlytics
6 | import com.google.firebase.database.FirebaseDatabase
7 | import com.squareup.leakcanary.LeakCanary
8 | import io.fabric.sdk.android.Fabric
9 |
10 | @Moddable
11 | class Application : MultiDexApplication() {
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | if (LeakCanary.isInAnalyzerProcess(this)) {
16 | // This process is dedicated to LeakCanary for heap analysis.
17 | // You should not init your app in this process.
18 | return
19 | }
20 | LeakCanary.install(this)
21 | FirebaseDatabase.getInstance().setPersistenceEnabled(!BuildConfig.DEBUG)
22 | Fabric.with(this, Crashlytics())
23 |
24 | ApplicationMods.apply(this)
25 | }
26 | }
--------------------------------------------------------------------------------
/cloudfunctions/functions/search.js:
--------------------------------------------------------------------------------
1 | const PAGES = [
2 | // Anime kai
3 | "animekaionline.com",
4 | "animeskai.com",
5 | "animakai.info",
6 | // Animes Online Br
7 | "animesonlinebr.com.br",
8 | //Animes Orion
9 | "animesorion.site",
10 | "animesorion.tv",
11 | "animesorion.video",
12 | // Anitube brasil
13 | "animetubebrasil.com",
14 | // Anitube Br
15 | "anitubebr.com",
16 | // Anitube site
17 | "anitube.site",
18 | // One Piece Ex
19 | "onepiece-ex.com.br",
20 | "one-piece-x.com.br",
21 | // Anima/tv Curse
22 | "tvcurse.com",
23 | "animacurse.moe",
24 | "animacurse.tv",
25 | // XVideos
26 | "xvideos.com"
27 | ]
28 | exports.search = (req, res) => {
29 | var query = req.query.query + " "+ PAGES.map( (p) => "site:"+p)
30 | .join(" OR ");
31 |
32 | res.redirect("http://google.com/search?q="+encodeURI(query))
33 | };
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnimeTubeBrasilFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import org.junit.runner.RunWith
9 | import resource_helper.Resources
10 |
11 | @RunWith(Spectrum::class)
12 | class AnimeTubeBrasilFactoryTest {
13 |
14 | companion object {
15 | val VALID_URLS = arrayOf("https://animetubebrasil.com/1582/")
16 | val INVALID_URLS = arrayOf("anitub")
17 | }
18 |
19 | init {
20 | FactoryChecker.describe(AnimeTubeBrasilFactory) {
21 | whenEpisode(Resources.responses.animetubebrasil.playerEpisodesJson.loadEpisodeResource())
22 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server-ktor/build.gradle:
--------------------------------------------------------------------------------
1 | group 'com.brunodles.animewatcher.serverktor'
2 | version '0.1.0'
3 |
4 | apply plugin: 'java'
5 | apply plugin: 'kotlin'
6 | apply plugin: 'application'
7 |
8 | mainClassName = 'com.brunodles.animewatcher.serverktor.MainKt'
9 |
10 | sourceCompatibility = 1.8
11 | compileKotlin { kotlinOptions.jvmTarget = "1.8" }
12 | compileTestKotlin { kotlinOptions.jvmTarget = "1.8" }
13 |
14 | kotlin { experimental { coroutines "enable" } }
15 |
16 | dependencies {
17 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
18 | implementation "io.ktor:ktor-server-netty:$ktor_version"
19 | implementation "ch.qos.logback:logback-classic:1.2.3"
20 |
21 | implementation "io.ktor:ktor-gson:$ktor_version"
22 | implementation 'com.google.code.gson:gson:2.8.2'
23 |
24 | implementation project(":explorer")
25 |
26 | testImplementation 'junit:junit:4.12'
27 | testImplementation "io.ktor:ktor-server-test-host:$ktor_version"
28 | }
29 |
30 | task stage {
31 | dependsOn installDist
32 | }
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # AnimeWatcher Android
2 |
3 | ### Instalation
4 | This project does not contains release nor provide apk for you.
5 | (At least, yet.)
6 | To use this you will have to checkout the code and compile it by yourself.
7 |
8 | [Instructions](#instalation-instructions)
9 |
10 | ### Usage
11 | The idea is to detect automatically the pages that the user is trying to reach and then the app will open with the content.
12 |
13 | Just use your browser as usual, the app should detect those pages.
14 | You can also share the page with the app, if it's decodable it will show you the right content otherwise the url will be sent to us adding it as issue for future release.
15 |
16 | ### Instalation Instructions
17 | We're planning to do releases here on github, but by now you can follow these steps:
18 | 1. Clone the repo or download the source
19 | 1. Create a project on [firebase](https://firebase.google.com/)
20 | 1. Download it's `google-services.json` file on project folder
21 | 1. Plug your device on usb
22 | 1. Run `./gradlew app:installRelease`
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/persistence/Preferences.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.persistence
2 |
3 | import android.content.Context
4 | import android.preference.PreferenceManager
5 |
6 | class Preferences(private val context: Context) {
7 |
8 | companion object {
9 | private val KEY_URL = "URL"
10 | private val KEY_IMAGE_URL = "IMAGE_URL"
11 | }
12 |
13 | fun setUrl(url: String) =
14 | preferences()
15 | .edit()
16 | .putString(KEY_URL, url)
17 | .apply()
18 |
19 | fun getUrl() = preferences().getString(KEY_URL, null)
20 |
21 | fun setLastAnimeImage(url: String) =
22 | preferences()
23 | .edit()
24 | .putString(KEY_IMAGE_URL, url)
25 | .apply()
26 |
27 | fun getLastAnimeImage() =
28 | preferences()
29 | .getString(KEY_IMAGE_URL, "")
30 |
31 | private fun preferences() = PreferenceManager.getDefaultSharedPreferences(context)
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
19 |
20 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/home.xml:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/app/src/debug/kotlin/brunodles/animewatcher/LocalCrashes.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import android.os.Environment
4 | import com.brunodles.environmentmods.annotation.ModFor
5 | import java.io.File
6 |
7 |
8 | object LocalCrashes {
9 |
10 | @JvmStatic
11 | @ModFor(Application::class)
12 | fun installLocalCrashHandler(application: Application) {
13 | val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
14 | Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
15 | val dir = File(Environment.getExternalStorageDirectory(), application.packageName)
16 | if (!dir.exists())
17 | dir.mkdirs()
18 | val file = File(dir, "${System.currentTimeMillis()}-${throwable.message}")
19 | val stackTrace = throwable.stackTrace.joinToString(" ")
20 | file.writeText(stackTrace)
21 | exceptionHandler.uncaughtException(thread, throwable)
22 | }
23 | }
24 |
25 | fun isExternalStorageWritable(): Boolean
26 | = Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
27 |
28 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/bruno/android-sdk-linux/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
27 |
28 | # Firebase
29 | -keepattributes Signature
30 | -keepclassmembers class bruno.animewatcher.episode.** {
31 | *;
32 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/explorer/Episode.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.explorer
2 |
3 | import java.io.Serializable
4 |
5 | data class Episode(
6 | val description: String,
7 | val number: Int,
8 | val animeName: String? = null,
9 | val image: String? = null,
10 | val video: String? = null,
11 | val link: String,
12 | val nextEpisodes: List = arrayListOf(),
13 | val temporaryVideoUrl: Boolean = false) : Serializable {
14 |
15 | constructor() : this("", 0, link = "")
16 |
17 | companion object {
18 | private const val serialVersionUid: Long = 1L
19 | }
20 |
21 | fun toMap() = mapOf(
22 | "description" to description,
23 | "number" to number,
24 | "animeName" to animeName,
25 | "image" to image,
26 | "video" to video,
27 | "link" to link,
28 | "nextEpisodes" to nextEpisodes,
29 | "temporaryVideoUrl" to temporaryVideoUrl
30 | ).filterValues { it != null }
31 |
32 | fun containsNextEpisodes() = !nextEpisodes.isEmpty()
33 |
34 | fun isPlayable() = video != null
35 | }
36 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/explorer/Episode.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.explorer
2 |
3 | import brunodles.kotlin.annotation.NoArgs
4 | import java.io.Serializable
5 |
6 | @NoArgs
7 | data class Episode(
8 | val description: String,
9 | val number: Int,
10 | val animeName: String? = null,
11 | val image: String? = null,
12 | val video: String? = null,
13 | val link: String,
14 | val nextEpisodes: List = arrayListOf(),
15 | val temporaryVideoUrl: Boolean = false) : Serializable {
16 |
17 | companion object {
18 | private const val serialVersionUid: Long = 1L
19 | }
20 |
21 | fun toMap() = mapOf(
22 | "description" to description,
23 | "number" to number,
24 | "animeName" to animeName,
25 | "image" to image,
26 | "video" to video,
27 | "link" to link,
28 | "nextEpisodes" to nextEpisodes,
29 | "temporaryVideoUrl" to temporaryVideoUrl
30 | ).filterValues { it != null }
31 |
32 | fun containsNextEpisodes() = !nextEpisodes.isEmpty()
33 |
34 | fun isPlayable() = video != null
35 | }
36 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/testhelper/ResourceFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.testhelper
2 |
3 | import brunodles.loadResource
4 | import brunodles.urlfetcher.CacheFetcher.Companion.urlToKey
5 | import brunodles.urlfetcher.Logger
6 | import brunodles.urlfetcher.UrlFetcher
7 | import org.jsoup.Jsoup
8 | import org.jsoup.nodes.Document
9 |
10 | internal class ResourceFetcher(private val nestedFetcher: UrlFetcher) : UrlFetcher {
11 |
12 | override fun get(url: String): Document {
13 | val key = urlToKey(url)
14 | try {
15 | return Jsoup.parse(loadPage(key))
16 | } catch (_: Exception) {
17 | }
18 | try {
19 | return nestedFetcher.get(url)
20 | } catch (e: Throwable) {
21 | throw WrappedException(key, e)
22 | }
23 | }
24 |
25 | companion object {
26 |
27 | private fun loadPage(key: String): String {
28 | Logger.log { "loadPage $key" }
29 | return loadResource(key)
30 | }
31 | }
32 |
33 | class WrappedException(cachePath: String, cause: Throwable) :
34 | RuntimeException("Failed to fetch cache using $cachePath", cause)
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_empty.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
25 |
26 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/explorer/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 | apply plugin: "kotlin-noarg"
3 | apply from: rootProject.file('gradle/buildconfig.gradle')
4 | apply from: rootProject.file('gradle/resource_helper.gradle')
5 |
6 | noArg {
7 | annotation("brunodles.kotlin.annotation.NoArgs")
8 | }
9 |
10 | buildconfig {
11 | targetPackage = "brunodles.animewatcher.explorer"
12 | field "String", "ROOT_DIR", "\"${project.rootDir.getPath()}\""
13 | field "Boolean", "USE_CACHE", Boolean.parseBoolean(project.properties.get("usecache", "true"))
14 | field "Boolean", "UPDATE_CACHE", Boolean.parseBoolean(project.properties.get("updatecache", "false"))
15 |
16 | toggler.stringPropertyNames().each {
17 | field "Boolean", it.toUpperCase(), toggler.getProperty(it)
18 | }
19 | }
20 |
21 | dependencies {
22 | compile 'com.brunodles:alchemy-alchemist:2.1.0'
23 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
24 | implementation 'com.google.code.gson:gson:2.8.2'
25 |
26 | testImplementation 'junit:junit:4.12'
27 | testImplementation 'com.greghaskins:spectrum:1.1.1'
28 | testImplementation 'com.github.tomakehurst:wiremock:2.18.0'
29 | }
30 |
31 | sourceCompatibility = "1.7"
32 | targetCompatibility = "1.7"
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/cast/MultiCaster.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | import android.app.Activity
4 | import android.support.v7.app.MediaRouteButton
5 | import android.widget.ImageButton
6 | import brunodles.animewatcher.BuildConfig
7 | import brunodles.animewatcher.explorer.Episode
8 |
9 | internal class MultiCaster(
10 | activity: Activity,
11 | mediaRouteButton: MediaRouteButton,
12 | imageButton: ImageButton
13 | ) :
14 | Caster {
15 |
16 | companion object {
17 | val TAG = "MultiCaster"
18 | }
19 |
20 | val casters = ArrayList()
21 | var current: Caster? = null
22 |
23 | init {
24 | if (BuildConfig.CONNECT_SDK)
25 | addCaster(ConnectSdkCasterFactory.create(activity, imageButton) { current = it })
26 | addCaster(GoogleCaster(activity, mediaRouteButton) { current = it })
27 | }
28 |
29 | private fun addCaster(caster: Caster) {
30 | casters.add(caster)
31 | current = caster
32 | }
33 |
34 | override fun playRemote(currentEpisode: Episode, position: Long) {
35 | current?.playRemote(currentEpisode, position)
36 | }
37 |
38 | override fun isConnected(): Boolean = current?.isConnected() ?: false
39 | }
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/OnePieceXFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import org.junit.runner.RunWith
9 | import resource_helper.Resources
10 |
11 | @RunWith(Spectrum::class)
12 | class OnePieceXFactoryTest {
13 |
14 | companion object {
15 |
16 | val VALID_URLS = arrayOf(
17 | "https://onepiece-ex.com.br/episodios/online/208/",
18 | "http://onepiece-ex.com.br/episodios/online/207/",
19 | "onepiece-ex.com.br/episodios/online/209"
20 | )
21 | val INVALID_URLS = arrayOf(
22 | "https://one-piece-x.com.br/episodios/t04/",
23 | "one-piece-x.com.br/episodios/online"
24 | )
25 | }
26 |
27 | init {
28 | FactoryChecker.describe(OnePieceXFactory) {
29 | whenEpisode(Resources.responses.onepiecex.playerEpisodesJson.loadEpisodeResource())
30 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/decoders/anitubex/src/test/kotlin/brunodles/anitubex/AnitubexFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.anitubex
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import brunodles.animewatcher.testhelper.FactoryChecker
5 | import com.greghaskins.spectrum.Spectrum
6 | import org.junit.runner.RunWith
7 |
8 | @RunWith(Spectrum::class)
9 | class AnitubexFactoryTest {
10 |
11 | companion object {
12 | val VALID_URLS = arrayOf("http://www.anitubex.com/one-piece-1",
13 | "https://www.anitubex.com/one-piece-1")
14 | val INVALID_URLS = arrayOf("anitub")
15 | val currentEpisode = Episode(
16 | number = 1,
17 | description = "One Piece 1",
18 | link = "http://www.anitubex.com/one-piece-1",
19 | video = "http://www.blogger.com/video-play.mp4?contentId=3eb2b428663ef38f",
20 | nextEpisodes = arrayListOf(
21 | Episode(number = 2,
22 | description = "Proximo Episódio",
23 | link = "http://www.anitubex.net/video/66822"
24 | )
25 | )
26 | )
27 | }
28 |
29 | init {
30 | FactoryChecker.checkFactory(AnitubexFactory, VALID_URLS, INVALID_URLS, currentEpisode)
31 | }
32 | }
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnitubeBrFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import com.greghaskins.spectrum.Spectrum.xit
9 | import org.junit.runner.RunWith
10 | import resource_helper.Resources
11 |
12 | @RunWith(Spectrum::class)
13 | class AnitubeBrFactoryTest {
14 |
15 | companion object {
16 | val VALID_URLS = arrayOf(
17 | "https://www.anitubebr.com/vd/19249/",
18 | "https://anitubebr.com/vd/19249/",
19 | "http://www.anitubebr.com/vd/19249/",
20 | "http://anitubebr.com/vd/19249/",
21 | "www.anitubebr.com/vd/19249/",
22 | "anitubebr.com/vd/19249/"
23 | )
24 | val INVALID_URLS = arrayOf("anitub")
25 | }
26 |
27 | init {
28 | FactoryChecker.describe(AnitubeBrFactory) {
29 | whenEpisode(Resources.responses.anitubebr.playerEpisodesJson.loadEpisodeResource())
30 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
31 | xit("check video url") {}
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/server-spark/server_blueprint.md:
--------------------------------------------------------------------------------
1 | FORMAT: 1A
2 | HOST: https://anime-watcher-spark.herokuapp.com
3 |
4 | # AnimeWatcher
5 |
6 | An api to decode pages into a simple model.
7 |
8 | ## Decoder [/decoder]
9 |
10 | ### Decode Page [POST]
11 |
12 | With this action, you can decode a url into a page info.
13 | It takes a raw string to search for useful content.
14 |
15 | + Request (text/plain)
16 |
17 | https://www.animesorion.video/60417
18 |
19 | + Response 200 (application/json)
20 |
21 | + Body
22 |
23 | {
24 | "description":"Assistir Dragon Ball Super Dublado Episódio 1",
25 | "number":1,
26 | "animeName":"Dragon Ball Super Dublado",
27 | "video":"https://www.blogger.com/video-play.mp4?contentId\u003d8ca7f7f864406273",
28 | "link":"https://www.animesorion.video/60417",
29 | "nextEpisodes":[
30 | {
31 | "description":"Next",
32 | "number":2,
33 | "animeName":"Dragon Ball Super Dublado",
34 | "link":"https://www.animesorion.video/114084",
35 | "nextEpisodes":[
36 |
37 | ],
38 | "temporaryVideoUrl":false
39 | }
40 | ],
41 | "temporaryVideoUrl":false
42 | }
43 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnimesOnlineBrFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import org.junit.runner.RunWith
9 | import resource_helper.Resources
10 |
11 | @RunWith(Spectrum::class)
12 | class AnimesOnlineBrFactoryTest {
13 |
14 | companion object {
15 |
16 | val VALID_URLS = arrayOf(
17 | "https://www.animesonlinebr.com.br/video/50034",
18 | "https://www.animesonlinebr.com.br/desenho/1909"
19 | )
20 | val INVALID_URLS = emptyArray()
21 | }
22 |
23 | init {
24 | FactoryChecker.describe(AnimesOnlineBrFactory) {
25 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
26 | Spectrum.describe("when episode page") {
27 | whenEpisode(Resources.responses.animesonlinebr.playerEpisodesJson.loadEpisodeResource())
28 | }
29 | Spectrum.describe("when anime about page") {
30 | whenEpisode(Resources.responses.animesonlinebr.aboutPageEpisodesJson.loadEpisodeResource())
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/XvideosFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import brunodles.urlfetcher.UrlFetcher
8 | import com.greghaskins.spectrum.Spectrum
9 | import com.greghaskins.spectrum.Spectrum.xit
10 | import org.junit.runner.RunWith
11 | import resource_helper.Resources
12 |
13 | @RunWith(Spectrum::class)
14 | class XvideosFactoryTest {
15 |
16 | companion object {
17 | val VALID_URLS = arrayOf(
18 | "https://www.xvideos.com/video12026193/anita_troca_o_peluche_pelo_pau",
19 | "http://www.xvideos.com/video12026193/anita_troca_o_peluche_pelo_pau",
20 | "xvideos.com/video12026193/anita_troca_o_peluche_pelo_pau"
21 | )
22 | val INVALID_URLS = emptyArray()
23 | }
24 |
25 | init {
26 | FactoryChecker.describe(XvideosFactory) {
27 | if (UrlFetcher.useCache)
28 | whenEpisode(Resources.responses.xvideos.playerEpisodesJson.loadEpisodeResource())
29 | else
30 | xit("when episode") {}
31 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cloudfunctions/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 |
3 | //const searchModule = require("./search")
4 |
5 | const admin = require('firebase-admin');
6 | admin.initializeApp()
7 |
8 | const invalidTextRegex = /[^\d\w]+/g;
9 |
10 |
11 | //exports.search = functions.https.onRequest(searchModule.search);
12 |
13 | exports.onAddToHistory = functions.database
14 | .ref("/users/{uid}/history/{pushId}")
15 | .onCreate((snapshot, context) => {
16 |
17 | const userId = context.auth.uid;
18 | const episodeUrl = snapshot.val();
19 | const episodeId = episodeUrl.replace(invalidTextRegex, "");
20 |
21 | console.log("user: "+userId +", episodeUrl: "+episodeUrl+", episodeId: "+episodeId);
22 |
23 | return admin.database().ref("/videos/"+episodeId).once("value").then( episodeRef => {
24 |
25 | const episode = episodeRef.val();
26 |
27 | console.log("episode: ", JSON.stringify(episode));
28 |
29 | return admin.database().ref("/users/"+userId+"/historyLast").set(episode)
30 | .then(res => {
31 | if (episode.nextEpisodes !== undefined && episode.nextEpisodes.length > 0)
32 | return admin.database()
33 | .ref("/users/" + userId + "/next/" + episode.animeName)
34 | .set(episode.nextEpisodes[0]);
35 | return res;
36 | });
37 | })
38 | });
39 |
--------------------------------------------------------------------------------
/gradle/sites.gradle:
--------------------------------------------------------------------------------
1 | def SUBDOMAINS = ["", "www."]
2 |
3 | def PAGES = [
4 | // Anime kai
5 | "animekaionline.com",
6 | "animeskai.com",
7 | "animakai.info",
8 | // Animes Online Br
9 | "animesonlinebr.com.br",
10 | //Animes Orion
11 | "animesorion.site",
12 | "animesorion.tv",
13 | "animesorion.video",
14 | // Anitube brasil
15 | "animetubebrasil.com",
16 | // Anitube Br
17 | "anitubebr.com",
18 | // Anitube site
19 | "anitube.site",
20 | // One Piece Ex
21 | "onepiece-ex.com.br",
22 | "one-piece-x.com.br",
23 | // Anima/tv Curse
24 | "tvcurse.com",
25 | "animacurse.moe",
26 | "animacurse.tv",
27 | // XVideos
28 | "xvideos.com"
29 | ]
30 |
31 | ext.DOMAINS = PAGES.collectMany { domain ->
32 | SUBDOMAINS.collect { it + domain }
33 | }
34 |
35 | ext.GOOGLE_SEARCH_PAGES_QUERY = PAGES.collect { "site:$it" }.join(" OR ")
36 |
37 | /**
38 | * Use `firebase database:set "/pages"` on firebase cli to override data.
39 | * This will enabled all known pages.
40 | */
41 | task firebasePages {
42 | description "Prints a json for pages. The output should be used to override firebase pages reference."
43 | group "pages"
44 | doLast {
45 | println "{\n" + PAGES.collect {
46 | "\t\"${it.replace('.', '_')}\":{\"url\":\"$it\",\"enabled\":\"true\"}"
47 | }.join(",\n") + "\n}"
48 | }
49 | }
--------------------------------------------------------------------------------
/server-spark/src/main/kotlin/com/brunodles/videowatcher/serverspark/ApplicationRouter.kt:
--------------------------------------------------------------------------------
1 | package com.brunodles.videowatcher.serverspark
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.decoders.UrlChecker
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.urlfetcher.UrlFetcher
7 | import com.google.gson.Gson
8 | import spark.Spark.path
9 | import spark.kotlin.RouteHandler
10 | import spark.kotlin.get
11 | import spark.kotlin.port
12 | import spark.kotlin.post
13 |
14 | fun main(args: Array) {
15 | port(getHerokuAssignedPort())
16 | AlchemistFactory.setUrlFetcher(UrlFetcher.composableFetcher())
17 | startServer()
18 | }
19 |
20 | val gson = Gson()
21 |
22 | fun startServer() {
23 | // Deprecated
24 | post("/decoder") {
25 | decoder { body() }
26 | }
27 | path("/v1") {
28 | get("/decoder") {
29 | decoder { queryParams("url") }
30 | }
31 | }
32 | }
33 |
34 | private fun RouteHandler.decoder(urlParameter: spark.Request.() -> String): Any {
35 | val episode: Episode?
36 | response.type("Application/json;charset=utf-8")
37 | try {
38 | episode = UrlChecker.videoInfo(urlParameter.invoke(request))
39 | } catch (e: Exception) {
40 | e.printStackTrace()
41 | this.response.status(500)
42 | return gson.toJson(e)
43 | }
44 | return gson.toJson(episode)
45 | }
46 |
47 | fun getHerokuAssignedPort(): Int = System.getenv("PORT")?.toInt() ?: 4567
48 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/TvCurseFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import com.greghaskins.spectrum.Spectrum.describe
9 | import org.junit.runner.RunWith
10 | import resource_helper.Resources
11 |
12 | @RunWith(Spectrum::class)
13 | class TvCurseFactoryTest {
14 |
15 | companion object {
16 | val VALID_URLS = arrayOf(
17 | "https://tvcurse.com/?p=713",
18 | "http://tvcurse.com/?p=713",
19 | "tvcurse.com?p=123",
20 | "http://tvcurse.com?p=321",
21 | "animacurse.moe/?p=713",
22 | "animacurse.tv/?p=713"
23 | )
24 | val INVALID_URLS = arrayOf("tvcurse.com/?cat=123")
25 | }
26 |
27 | init {
28 | FactoryChecker.describe(TvCurseFactory) {
29 | describe("when page contains next episodes") {
30 | whenEpisode(Resources.responses.tvcurse.playerWithNextEpisodesJson.loadEpisodeResource())
31 | }
32 | describe("when page does not contains next") {
33 | whenEpisode(Resources.responses.tvcurse.playerWithoutNextEpisodesJson.loadEpisodeResource())
34 | }
35 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/adapter/ViewDataBindingAdapter.kt:
--------------------------------------------------------------------------------
1 | package brunodles.adapter
2 |
3 | import android.databinding.DataBindingUtil
4 | import android.databinding.ViewDataBinding
5 | import android.support.annotation.LayoutRes
6 | import android.support.v7.widget.RecyclerView
7 | import android.view.LayoutInflater
8 | import android.view.ViewGroup
9 | import java.util.*
10 |
11 | class ViewDataBindingAdapter- (
12 | @LayoutRes private val layoutId: Int,
13 | private val onBind: (ViewHolder, ITEM, Int) -> Unit) :
14 | RecyclerView.Adapter>() {
15 | var layoutInflater: LayoutInflater? = null
16 |
17 | var list: List
- = Collections.emptyList()
18 | set(value) {
19 | field = value
20 | notifyDataSetChanged()
21 | }
22 |
23 | override fun onCreateViewHolder(viewGroup: ViewGroup, index: Int): ViewHolder {
24 | val context = viewGroup.context
25 | if (layoutInflater == null)
26 | layoutInflater = LayoutInflater.from(context)
27 | val binding = DataBindingUtil.inflate(layoutInflater!!, layoutId, viewGroup, false)
28 | return ViewHolder(binding)
29 | }
30 |
31 | override fun onBindViewHolder(holder: ViewHolder, index: Int) {
32 | onBind(holder, list[index], index)
33 | }
34 |
35 | override fun getItemCount(): Int = list.size
36 |
37 | class ViewHolder(val binder: BINDER) : RecyclerView.ViewHolder(binder.root)
38 | }
--------------------------------------------------------------------------------
/cloudfunctions/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,firebase
3 |
4 | ### Firebase ###
5 | .idea
6 | **/node_modules/*
7 | **/.firebaserc
8 |
9 | ### Node ###
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # TypeScript v1 declaration files
49 | typings/
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional REPL history
58 | .node_repl_history
59 |
60 | # Output of 'npm pack'
61 | *.tgz
62 |
63 | # Yarn Integrity file
64 | .yarn-integrity
65 |
66 | # dotenv environment variables file
67 | .env
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless
83 |
84 |
85 | # End of https://www.gitignore.io/api/node,firebase
86 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/AlchemistFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import brunodles.urlfetcher.UrlFetcher
4 | import com.brunodles.alchemist.Alchemist
5 | import com.brunodles.alchemist.AnnotationInvocation
6 | import com.brunodles.alchemist.AnnotationTransmutation
7 | import com.brunodles.alchemist.TransmutationsBook
8 |
9 | object AlchemistFactory {
10 |
11 | private var urlFetcher: UrlFetcher? = null
12 | val alchemist: Alchemist by lazy {
13 | System.setProperty(
14 | "user-agent",
15 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
16 | )
17 | Alchemist.Builder()
18 | .uriResolver {
19 | getUrlFetcher().get(it).html()
20 | }
21 | .transformers(
22 | TransmutationsBook.Builder()
23 | .add(ToIntTransmutation())
24 | .build()
25 | )
26 | .build()
27 | }
28 |
29 | fun getUrlFetcher(): UrlFetcher {
30 | urlFetcher?.let { return it }
31 | return UrlFetcher.fetcher()
32 | }
33 |
34 | fun setUrlFetcher(urlFetcher: UrlFetcher) {
35 | this.urlFetcher = urlFetcher
36 | }
37 | }
38 |
39 | @Retention(AnnotationRetention.RUNTIME)
40 | @Target(AnnotationTarget.FUNCTION)
41 | annotation class ToInt
42 |
43 | class ToIntTransmutation : AnnotationTransmutation, List> {
44 |
45 | override fun transform(value: AnnotationInvocation>): List =
46 | value.result.map(String::toInt).toList()
47 | }
48 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/UrlFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import brunodles.animewatcher.explorer.BuildConfig
4 | import org.jsoup.nodes.Document
5 |
6 | /**
7 | * A Class to fetch urls.
8 | */
9 | interface UrlFetcher {
10 |
11 | fun get(url: String): Document
12 |
13 | companion object {
14 | val useCache: Boolean = BuildConfig.USE_CACHE
15 | var cacheDir = BuildConfig.ROOT_DIR
16 | var useLog = false
17 | /** Overrides result of fetcher method */
18 | var fetcherOverride: UrlFetcher? = null
19 |
20 | /**
21 | * Returns an instance of UrlFetcher.
22 | * This instance may handle caches.
23 | * @See useCache
24 | */
25 | fun fetcher(): UrlFetcher {
26 | fetcherOverride?.let { return it }
27 | val urlFetcherComposable = composableFetcher()
28 | if (useCache)
29 | urlFetcherComposable.withCache()
30 | .withJsRedirect()
31 | return urlFetcherComposable
32 | }
33 |
34 | /**
35 | * Returns an instance of a UrlFetcher that can be composable.
36 | * With it you can compose multiple behaviours of UrlFetcher, like:
37 | * * Cache
38 | * * Follow Redirects
39 | */
40 | fun composableFetcher() = UrlFetcherComposable()
41 |
42 | @Deprecated(
43 | "This does not support composable configuration and always use GET",
44 | ReplaceWith("composableFetcher"), DeprecationLevel.ERROR
45 | )
46 | fun fetchUrl(url: String): Document = fetcher().get(url)
47 | }
48 | }
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | working_directory: ~/code
5 | docker:
6 | - image: circleci/android:api-27-alpha
7 | environment:
8 | JVM_OPTS: -Xmx3200m
9 | steps:
10 | - checkout
11 |
12 | - restore_cache:
13 | keys:
14 | - v1-dependencies-{{ checksum "build.gradle" }}
15 | # Fallback
16 | - v1-dependencies-
17 |
18 | - run:
19 | name: Assemble
20 | command: ./gradlew clean app:googleServicesDecrypt assembleDebug
21 | - run:
22 | name: Linters
23 | command: ./gradlew lint ktlint checkstyleMain --continue -x app:ktlint
24 | - run:
25 | name: Tests - Local
26 | command: ./gradlew cleanTest test -Pusecache=true -PparallelForks=1
27 | - run:
28 | name: Coverage
29 | command: ./gradlew jacocoTestReport
30 |
31 | - save_cache:
32 | paths:
33 | - ~/.gradle
34 | key: v1-dependencies-{{ checksum "build.gradle" }}
35 |
36 | - store_artifacts:
37 | path: ./app/build/reports
38 | destination: app-reports
39 | - store_artifacts:
40 | path: ./explorer/build/reports
41 | destination: explorer-reports
42 | - store_artifacts:
43 | path: ./server-ktor/build/reports
44 | destination: server-reports
45 |
46 | workflows:
47 | version: 2
48 | workflow:
49 | jobs:
50 | - build
51 | nightly:
52 | triggers:
53 | - schedule:
54 | cron: "0 0 * * *"
55 | filters:
56 | branches:
57 | only:
58 | - master
59 | - develop
60 | jobs:
61 | - build
62 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnimaKaiFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import com.greghaskins.spectrum.Spectrum.describe
9 | import org.junit.runner.RunWith
10 | import resource_helper.Resources
11 |
12 | @RunWith(Spectrum::class)
13 | class AnimaKaiFactoryTest {
14 |
15 | companion object {
16 | val VALID_URLS = arrayOf(
17 | "https://www.animekaionline.com/tsukipro-the-animation/episodio-1",
18 | "http://www.animeskai.com/himouto-umaru-chan/ep-1",
19 | "https://www.animeskai.net/himouto-umaru-chan/ep-1",
20 | "animakai.info/himouto-umaru-chan/ep-1"
21 | )
22 | val INVALID_URLS = emptyArray()
23 | }
24 |
25 | init {
26 | FactoryChecker.describe(AnimaKaiFactory) {
27 | describe("when page returns 200 (Success)") {
28 | whenEpisode(Resources.responses.animakai.tskiproEpisodesJson.loadEpisodeResource())
29 | }
30 | describe("when page returns 500 (Server Error, but is a success)") {
31 | whenEpisode(Resources.responses.animakai.umaruchanEpisodesJson.loadEpisodeResource())
32 | }
33 | describe("when page is empty") {
34 | whenEpisode(Resources.responses.animakai.errorEpisodesJson.loadEpisodeResource())
35 | }
36 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/cli/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | sourceCompatibility = "1.7"
4 | targetCompatibility = "1.7"
5 |
6 | dependencies {
7 | compile project(':explorer')
8 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9 | }
10 |
11 | task decoder(type: JavaExec) {
12 | classpath sourceSets.main.runtimeClasspath
13 | main "com.brunodles.animewatcher.cli.DecoderKt"
14 | group "run"
15 | args project.findProperty("args") ?: []
16 | doFirst {
17 | if (!project.hasProperty("args")) {
18 | def errorMessage = "Missing page argument.\nWhen using with gradle: gradle cli:decoder -Pargs="
19 | throw new GradleException(errorMessage)
20 | }
21 | }
22 | }
23 |
24 | task googleSearchPagesQuery {
25 | description "Prints google search query for multiple sites"
26 | group "run"
27 | doLast {
28 | println GOOGLE_SEARCH_PAGES_QUERY
29 | }
30 | }
31 |
32 | task googleSearch {
33 | description "Prints google search url. Use \"query\" parameter to inform wanted text to search."
34 | group "run"
35 | doLast {
36 | def query = project.property("query")
37 | println "query: $query"
38 | println "http://google.com/search?q=" + URLEncoder.encode("$query $GOOGLE_SEARCH_PAGES_QUERY", "UTF-8")
39 | }
40 | }
41 |
42 | task intentFilterData {
43 | description "Prints all intent-filter data elements for all domains"
44 | group "run"
45 | doLast {
46 | println DOMAINS.collect { "" }.join("\n")
47 | }
48 | }
49 |
50 | task printDomains {
51 | description "Prints all domains"
52 | group "run"
53 | doLast {
54 | DOMAINS.each {
55 | println it
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/api/ApiFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.api
2 |
3 | import android.util.Log
4 | import brunodles.animewatcher.explorer.Episode
5 | import com.google.gson.Gson
6 | import com.google.gson.GsonBuilder
7 | import io.reactivex.Single
8 | import io.reactivex.schedulers.Schedulers
9 | import okhttp3.OkHttpClient
10 | import okhttp3.logging.HttpLoggingInterceptor
11 | import retrofit2.Retrofit
12 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
13 | import retrofit2.converter.gson.GsonConverterFactory
14 | import retrofit2.converter.scalars.ScalarsConverterFactory
15 | import retrofit2.http.Body
16 | import retrofit2.http.POST
17 |
18 | object ApiFactory {
19 |
20 | val TAG = "ApiFactory"
21 |
22 | val gson: Gson by lazy {
23 | GsonBuilder()
24 | .create()
25 | }
26 | val client: OkHttpClient by lazy {
27 | OkHttpClient.Builder()
28 | .addInterceptor(HttpLoggingInterceptor {
29 | Log.d(TAG, "okHttp: $it")
30 | }.setLevel(HttpLoggingInterceptor.Level.BODY))
31 | .build()
32 | }
33 | val retrofit: Retrofit by lazy {
34 | Retrofit.Builder()
35 | .baseUrl("https://anime-watcher-spark.herokuapp.com/")
36 | .addConverterFactory(ScalarsConverterFactory.create())
37 | .addConverterFactory(GsonConverterFactory.create(gson))
38 | .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
39 | .client(client)
40 | .build()
41 | }
42 | val api: Api by lazy {
43 | retrofit.create(Api::class.java)
44 | }
45 |
46 | interface Api {
47 |
48 | @POST("/decoder")
49 | fun decoder(@Body url: String): Single
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/player/PlayerListener.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.player
2 |
3 | import android.util.Log
4 | import com.google.android.exoplayer2.ExoPlaybackException
5 | import com.google.android.exoplayer2.ExoPlayer
6 | import com.google.android.exoplayer2.PlaybackParameters
7 | import com.google.android.exoplayer2.Timeline
8 | import com.google.android.exoplayer2.source.TrackGroupArray
9 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray
10 |
11 | internal class PlayerListener(val onPlayerStateChanged: (Int) -> Unit) : ExoPlayer.EventListener {
12 |
13 | companion object {
14 | val TAG = "PlayerListener"
15 | }
16 |
17 | override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {
18 | Log.d(TAG, "onPlaybackParametersChanged: $playbackParameters")
19 | }
20 |
21 | override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {
22 | Log.d(TAG, "onTracksChanged: ")
23 | }
24 |
25 | override fun onPlayerError(error: ExoPlaybackException?) {
26 | Log.d(TAG, "onPlayerError: $error")
27 | }
28 |
29 | override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
30 | Log.d(TAG, "onPlayerStateChanged: playWhenReady: $playWhenReady, playbackState: $playbackState")
31 | onPlayerStateChanged.invoke(playbackState)
32 | }
33 |
34 | override fun onLoadingChanged(isLoading: Boolean) {
35 | Log.d(TAG, "onLoadingChanged: $isLoading")
36 | }
37 |
38 | override fun onPositionDiscontinuity() {
39 | Log.d(TAG, "onPositionDiscontinuity: ")
40 | }
41 |
42 | override fun onTimelineChanged(timeline: Timeline?, manifest: Any?) {
43 | Log.d(TAG, "onTimelineChanged: $timeline")
44 | }
45 | }
--------------------------------------------------------------------------------
/server-spark/README.md:
--------------------------------------------------------------------------------
1 | # Anime Watcher - Server
2 | An api to decode pages into a simple model.
3 |
4 | ## Usage
5 | If you're planning to use this server you can follow these steps.
6 |
7 | ### 1 - Search for a anime on google
8 | You can use this
9 | [Sample Link](http://google.com/search?q=Dragon+Ball+Super+ep+01+site%3Aanimekaionline.com+OR+site%3Aanimeskai.com+OR+site%3Aanimakai.info+OR+site%3Aanimesonlinebr.com.br+OR+site%3Aanimesorion.site+OR+site%3Aanimesorion.tv+OR+site%3Aanimesorion.video+OR+site%3Aanimetubebrasil.com+OR+site%3Aanitubebr.com+OR+site%3Aanitube.site+OR+site%3Aonepiece-ex.com.br+OR+site%3Aone-piece-x.com.br+OR+site%3Atvcurse.com+OR+site%3Aanimacurse.moe+OR+site%3Aanimacurse.tv+OR+site%3Axvideos.com).
10 | It have filters for pages that we can handle.
11 |
12 | ### 2 - Grab the link of one result
13 | Select one result and copy its link, there is no need navigate to the page,
14 | this is what we're trying to avoid.
15 |
16 | Here is a sample:
17 | ```
18 | https://www.animesorion.video/60417
19 | ```
20 |
21 | ### 3 - Send the link to the server
22 | The link should be passed as `text/plain` on the request body.
23 | You should use `POST`.
24 | Be careful: do not send the url quoted, it will mess up the server.
25 |
26 | You can run a sample with this command bellow:
27 | ```bash
28 | curl https://anime-watcher-spark.herokuapp.com/decoder -d "https://www.animesorion.video/60417"
29 | ```
30 |
31 | ## Documentation
32 | We use [Api Blueprint](https://apiblueprint.org/) for documentation.
33 |
34 | Our documentation is [here](server_blueprint.md).
35 |
36 | You can use one [mock server](https://apiblueprint.org/tools.html#mock%20servers)
37 | to run the documentation as a server.
38 |
39 | If you want to see documentation in a better way, use a
40 | [renderer](https://apiblueprint.org/tools.html#renderers).
41 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/animewatcher/decoders/AnimesOrionFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.testhelper.FactoryChecker
4 | import brunodles.animewatcher.testhelper.FactoryChecker.whenCheckIsEpisode
5 | import brunodles.animewatcher.testhelper.FactoryChecker.whenEpisode
6 | import brunodles.loadEpisodeResource
7 | import com.greghaskins.spectrum.Spectrum
8 | import com.greghaskins.spectrum.Spectrum.describe
9 | import org.junit.runner.RunWith
10 | import resource_helper.Resources
11 |
12 | @RunWith(Spectrum::class)
13 | class AnimesOrionFactoryTest {
14 |
15 | companion object {
16 | val VALID_URLS = arrayOf(
17 | "http://www.animesorion.online/71672",
18 | "http://www.animesorion.tv/71672",
19 | "https://www.animesorion.site/71672",
20 | "https://www.animesorion.online/71808",
21 | "https://www.animesorion.org/71808",
22 | "https://www.animesorion.org/71808?blabalba",
23 | "https://www.animesorion.site/71672/18092"
24 | )
25 | val INVALID_URLS = emptyArray()
26 | }
27 |
28 | init {
29 | FactoryChecker.describe(AnimesOrionFactory) {
30 | describe("when single next episodes") {
31 | whenEpisode(Resources.responses.animesorion.singleNextEpisodesJson.loadEpisodeResource())
32 | }
33 | describe("when about page") {
34 | whenEpisode(Resources.responses.animesorion.aboutPageEpisodesJson.loadEpisodeResource())
35 | }
36 | describe("when no next episodes") {
37 | whenEpisode(Resources.responses.animesorion.noNextEpisodesJson.loadEpisodeResource())
38 | }
39 | whenCheckIsEpisode(VALID_URLS, INVALID_URLS)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/JsonMaker.kt:
--------------------------------------------------------------------------------
1 | import brunodles.animewatcher.decoders.XvideosFactoryTest
2 | import brunodles.animewatcher.explorer.BuildConfig
3 | import com.google.gson.Gson
4 | import org.junit.Ignore
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.junit.runners.JUnit4
8 | import java.io.File
9 | import kotlin.reflect.KClass
10 |
11 | @Ignore
12 | @RunWith(JUnit4::class)
13 | class JsonMaker {
14 | private val gson = Gson()
15 | private var dir: File? = null
16 | private var dirname: String? = null
17 |
18 | @Test
19 | fun main() {
20 | forClass(XvideosFactoryTest::class, XvideosFactoryTest) {
21 | // writeEpisode("player", currentEpisode)
22 | // writeEpisode("player_without_next", PLAYER_WITHOUT_NEXT)
23 | }
24 | }
25 |
26 | private fun forClass(factoryTest: KClass, obj: T, func: T.() -> Unit) {
27 | dirname = factoryTest.java.simpleName.replace("FactoryTest", "")
28 | .toLowerCase()
29 | dir =
30 | File(BuildConfig.ROOT_DIR, "/explorer/src/test/resources/$dirname")
31 | func.invoke(obj)
32 | }
33 |
34 | private fun writeEpisode(prefix: String, obj: Any) =
35 | writeFile(prefix + "_episodes.json", obj)
36 |
37 | private fun writeFile(filename: String, obj: Any) {
38 | val json = gson.toJson(obj)
39 | // println("$filename = $json")
40 | val fieldName =
41 | filename.split(Regex("[^a-zA-Z0-9]+"))
42 | .asSequence()
43 | .map { it.capitalize() }
44 | .joinToString("")
45 | .decapitalize()
46 | println("$filename = Resources.$dirname.$fieldName.loadEpisodeResource()")
47 | dir?.mkdirs()
48 | File(dir, filename).writeText(json)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/cloudfunctions/functions/gateway.js:
--------------------------------------------------------------------------------
1 | const Client = require('node-rest-client').Client;
2 | const client = new Client();
3 | client.on('error', (err) => {
4 | console.error('Something went wrong on the client', err);
5 | })
6 |
7 | const SERVER_URLS = [
8 | "https://anime-watcher-spark.herokuapp.com",
9 | "https://anime-watcher-ktor.herokuapp.com",
10 | ];
11 |
12 | exports.gateway = (req, res) => {
13 | var url = Object.keys(req.body)[0];
14 | console.log("Requested body: ", url);
15 | console.log("Requested url, ", req.url)
16 | return decode(0, "POST", req.url, {data:url}, res)
17 | };
18 |
19 | function decode(serverIndex, method, path, body, response) {
20 | var url = SERVER_URLS[serverIndex]+path;
21 | console.log("Server("+serverIndex+") = ", url);
22 |
23 | var errorHandler = (error) => {
24 | if (serverIndex + 1 >= SERVER_URLS.length){
25 | return response.status(500)
26 | .type('application/json')
27 | .send(error)
28 | } else {
29 | console.log("Server("+serverIndex+") erred, trying next")
30 | return decode(serverIndex+1, method, path, body, response);
31 | }
32 | }
33 |
34 | var successHandler = (data, res) => {
35 | console.log("Server("+serverIndex+")");
36 | console.log("\tStatusCode: ",res.statusCode);
37 | console.log("\tData: ", data)
38 |
39 | if (res.statusCode === 200) {
40 | return response.status(200)
41 | .type('application/json')
42 | .send(data);
43 | } else {
44 | return errorHandler(data)
45 | }
46 | }
47 |
48 | var call;
49 | if (method === "POST") {
50 | call = client.post(url, body, successHandler)
51 | } else {
52 | call = client.get(url, successHandler)
53 | }
54 | call.on('error', errorHandler)
55 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/AnimeTubeBrasilFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.ToInt
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.animewatcher.explorer.PageParser
7 | import com.brunodles.alchemist.collectors.AttrCollector
8 | import com.brunodles.alchemist.collectors.TextCollector
9 | import com.brunodles.alchemist.selector.Selector
10 |
11 | object AnimeTubeBrasilFactory : PageParser {
12 |
13 | private val EPISODE_URL_REGEX = Regex("(?:https?://)?(?:www\\.)?animetubebrasil\\.com/\\d+.*")
14 |
15 | override fun isEpisode(url: String): Boolean = url.matches(EPISODE_URL_REGEX)
16 |
17 | override fun episode(url: String): Episode {
18 | val currentEpisode = AlchemistFactory.alchemist.parseUrl(url, CurrentEpisode::class.java)
19 | with(currentEpisode) {
20 | return Episode(description(), number(), animeName(), null, video(), url,
21 | listOf(Episode("Next", number() + 1, animeName(),
22 | link = nextEpisode()))
23 | )
24 | }
25 | }
26 |
27 | interface CurrentEpisode {
28 |
29 | @Selector(".epTituloNn")
30 | @TextCollector
31 | @Regexp("^(?:.*?):\\s+?(.*)\$")
32 | fun description(): String
33 |
34 | @Selector(".epTituloNn")
35 | @TextCollector
36 | @Regexp("(\\d+)")
37 | @ToInt
38 | fun number(): Int
39 |
40 | @Selector(".epTituloTit")
41 | @TextCollector
42 | fun animeName(): String?
43 |
44 | @Selector("video source")
45 | @AttrCollector("src")
46 | fun video(): String?
47 |
48 | @Selector(".epVideoControl .epVideoControlItem:last-child a")
49 | @AttrCollector("href")
50 | fun nextEpisode(): String
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/decoders/animesproject/src/test/kotlin/brunodles/animesproject/AnimesProjectFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animesproject
2 |
3 | import brunodles.animewatcher.explorer.BuildConfig
4 | import brunodles.animewatcher.explorer.Episode
5 | import brunodles.animewatcher.testhelper.FactoryChecker
6 | import com.greghaskins.spectrum.Spectrum
7 | import org.junit.runner.RunWith
8 |
9 | @RunWith(Spectrum::class)
10 | @Suppress("ConstantConditionIf")
11 | class AnimesProjectFactoryTest {
12 |
13 | companion object {
14 | val VALID_URLS = arrayOf("https://animes.zlx.com.br/exibir/117/3440/one-piece-166")
15 | val INVALID_URLS = arrayOf("https://animes.zlx.com.br")
16 | val currentEpisode = Episode(
17 | number = 166,
18 | description = "Episódio 166",
19 | animeName = "One Piece",
20 | image = null,
21 | link = "https://animes.zlx.com.br/exibir/117/3440/one-piece-166",
22 | video = if (BuildConfig.USE_CACHE)
23 | "https://st01hd.animesproject.com.br/download/PIwE-qmHfqilp1P3GyMDOw/1514674597/O/one-piece/MQ/episodios/166.mp4"
24 | else
25 | "https://st01hd.animesproject.com.br/download/.*?/O/one-piece/MQ/episodios/166.mp4",
26 | nextEpisodes = arrayListOf(
27 | Episode(number = 167,
28 | description = "Episódio 167",
29 | link = "http://animes.zlx.com.br/exibir/117/3414/one-piece-167"),
30 | Episode(number = 168,
31 | description = "Episódio 168",
32 | link = "http://animes.zlx.com.br/exibir/117/3441/one-piece-168")
33 | ),
34 | temporaryVideoUrl = true
35 | )
36 | }
37 |
38 | init {
39 | FactoryChecker.checkFactory(AnimesProjectFactory, VALID_URLS, INVALID_URLS, currentEpisode)
40 | }
41 | }
--------------------------------------------------------------------------------
/explorer/src/test/resources/responses/tvcurse/player_with_next_episodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Assistir One Piece - Episódio 162 – Chopper em perigo! Antigo Deus x Sacerdote Shura! Online",
3 | "number": 162,
4 | "animeName": "One Piece",
5 | "image": "https://tvcurse.com/imgs/one-piece-episodio-162.webp",
6 | "video": "https://tvcurse.com/dd/ODJkMDU0ZDYzZjc5MzA5NQ\u003d\u003d",
7 | "link": "https://tvcurse.com/?p\u003d713",
8 | "nextEpisodes": [
9 | {
10 | "description": "Episódio 163 – Sempre Misteriosa! Provação das cordas e provação do amor!?",
11 | "number": 163,
12 | "animeName": "One Piece",
13 | "image": "https://tvcurse.com/imgs/one-piece-episodio-163.webp",
14 | "link": "https://tvcurse.com/?p\u003d714",
15 | "nextEpisodes": [],
16 | "temporaryVideoUrl": false
17 | },
18 | {
19 | "description": "Episódio 164 – Acendam a chama da sabedoria! Wiper, o Guerreiro",
20 | "number": 164,
21 | "animeName": "One Piece",
22 | "image": "https://tvcurse.com/imgs/one-piece-episodio-164.webp",
23 | "link": "https://tvcurse.com/?p\u003d715",
24 | "nextEpisodes": [],
25 | "temporaryVideoUrl": false
26 | },
27 | {
28 | "description": "Episódio 165 – Terra Flutuante de Ouro, Jaya! Para o Santuário de Deus!",
29 | "number": 165,
30 | "animeName": "One Piece",
31 | "image": "https://tvcurse.com/imgs/one-piece-episodio-165.webp",
32 | "link": "https://tvcurse.com/?p\u003d716",
33 | "nextEpisodes": [],
34 | "temporaryVideoUrl": false
35 | },
36 | {
37 | "description": "Episódio 166 – Véspera do Festival do Ouro! Afeta a Vearth",
38 | "number": 166,
39 | "animeName": "One Piece",
40 | "image": "https://tvcurse.com/imgs/one-piece-episodio-166.webp",
41 | "link": "https://tvcurse.com/?p\u003d717",
42 | "nextEpisodes": [],
43 | "temporaryVideoUrl": false
44 | }
45 | ],
46 | "temporaryVideoUrl": false
47 | }
--------------------------------------------------------------------------------
/cloudfunctions/README.md:
--------------------------------------------------------------------------------
1 | # Anime Watcher - Firebase Cloud Functions
2 |
3 | ## Gateway
4 | A gateway that can handle fallback to next server.
5 | The idea is to avoid handling errors on app, so when Heroku server reach
6 | the *dynos limit* we may use the next server.
7 |
8 | ### Usage
9 | Make the wanted request to
10 | ```
11 | https://us-central1-animewatcher-bbdf0.cloudfunctions.net/gateway/
12 | ```
13 |
14 | But append the wanted path to it, like this:
15 | ```
16 | https://us-central1-animewatcher-bbdf0.cloudfunctions.net/gateway/decoder
17 | ```
18 |
19 | You can also pass query and body parameters, it will fetch the right Heroku
20 | server.
21 |
22 | ### Issues
23 | Unfortunately google cloud functions does not support networking on free plan.
24 | The gateway is not working on firebase, only locally.
25 |
26 | ### Alternative?
27 | May it be useful, but we will need app integration.
28 |
29 | The idea is to use it to redirect to working server.
30 | Suggested flow:
31 | 1. client queries the cloud function.
32 | 2. cloud function checks current data, if exists **return** it.
33 | 3. cloud get the first enabled server.
34 | 4. user gets redirected to a working server.
35 | 5. if data is valid, client update Firebase videos cache list.
36 | 6. if data is invalid, client send `invalid server` to cloudfunctions and
37 | restart from step 1.
38 |
39 |
40 | ## Search
41 | A wrapper for google search engine, with filters.
42 |
43 | This function will redirect you to google search engine using filters for
44 | enabled pages.
45 |
46 | ### Objective
47 | * Avoid building search query on devices.
48 | * Dynamically include/remove working sites.
49 |
50 | ### Usage
51 | Make a `get` request to following url:
52 | ```
53 | https://us-central1-animewatcher-bbdf0.cloudfunctions.net/search?query=
54 | ```
55 |
56 | Sample with query `himout umaru chan R ep 04`
57 | ```
58 | https://us-central1-animewatcher-bbdf0.cloudfunctions.net/search?query=himouto%20umaru%20chan%20R%20ep%2004
59 | ```
60 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/AnimaKaiFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import brunodles.animewatcher.explorer.PageParser
5 | import brunodles.urlfetcher.UrlFetcher
6 | import brunodles.urlfetcher.src
7 | import org.jsoup.nodes.Document
8 |
9 | object AnimaKaiFactory : PageParser {
10 |
11 | private val EPISODE_URL_REGEX = Regex("(?:https?:\\/\\/)?(?:www\\.)?((animekaionline|animeskai)\\.(?:com|net)|animakai\\.info)\\/(.*?)\\/(episodio|ep)-\\d+")
12 | private val NUMBER_REGEX = Regex("\\d+")
13 | private val urlFetcher = UrlFetcher.fetcher()
14 |
15 | override fun isEpisode(url: String): Boolean = url.matches(EPISODE_URL_REGEX)
16 |
17 | override fun episode(url: String): Episode {
18 | val doc = urlFetcher.get(url)
19 | val video = doc.select(".box-video video")
20 | val src = video.select("source").src()
21 | return Episode(
22 | number = url.split("-").last().toIntOrNull() ?: 0,
23 | animeName = animeName(doc),
24 | image = imageUrl(doc),
25 | description = doc.select("[property=og:description]").attr("content"),
26 | video = src,
27 | link = url
28 | )
29 | }
30 |
31 | private fun imageUrl(doc: Document): String? {
32 | val url = doc.head()
33 | .select("meta[property=og:image]")
34 | ?.attr("content")
35 | ?.split("http")
36 | ?.last()
37 | if (url != null)
38 | return "http$url"
39 | return null
40 | }
41 |
42 | private fun animeName(doc: Document): String? =
43 | doc.head()
44 | .select("meta[property=og:description]")
45 | ?.attr("content")
46 | ?.split(" ")
47 | ?.filter { !it.matches(NUMBER_REGEX) }
48 | ?.joinToString(" ")
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/parcelable/EpisodeParcel.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.parcelable
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import brunodles.animewatcher.explorer.Episode
6 |
7 | data class EpisodeParcel(
8 | val description: String,
9 | val number: Int,
10 | val animeName: String? = null,
11 | val image: String? = null,
12 | val video: String? = null,
13 | val link: String,
14 | val nextEpisodes: List = arrayListOf()
15 | ) : Parcelable {
16 |
17 | constructor(parcel: Parcel) : this(
18 | parcel.readString(),
19 | parcel.readInt(),
20 | parcel.readString(),
21 | parcel.readString(),
22 | parcel.readString(),
23 | parcel.readString(),
24 | parcel.createTypedArrayList(CREATOR)
25 | )
26 |
27 | constructor(episode: Episode) : this(
28 | episode.description,
29 | episode.number,
30 | episode.animeName,
31 | episode.image,
32 | episode.video,
33 | episode.link,
34 | episode.nextEpisodes.asSequence().map { EpisodeParcel(it) }.toList()
35 | )
36 |
37 | override fun writeToParcel(parcel: Parcel, flags: Int) {
38 | parcel.writeString(description)
39 | parcel.writeInt(number)
40 | parcel.writeString(animeName)
41 | parcel.writeString(image)
42 | parcel.writeString(video)
43 | parcel.writeString(link)
44 | parcel.writeTypedList(nextEpisodes)
45 | }
46 |
47 | override fun describeContents(): Int {
48 | return 0
49 | }
50 |
51 | companion object CREATOR : Parcelable.Creator {
52 | override fun createFromParcel(parcel: Parcel): EpisodeParcel {
53 | return EpisodeParcel(parcel)
54 | }
55 |
56 | override fun newArray(size: Int): Array {
57 | return arrayOfNulls(size)
58 | }
59 | }
60 |
61 | fun isVideoMissing(): Boolean {
62 | return video == null
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/gradle/resource_helper.gradle:
--------------------------------------------------------------------------------
1 | File outputDir = new File(project.buildDir, "resource-helper")
2 | File inputDir = new File(project.projectDir, 'src/test/resources/')
3 | sourceSets.test.java.srcDirs outputDir
4 | String relativePath = inputDir.path + "/"
5 |
6 | task("compileResourcesClass") {
7 | doLast {
8 | def packageDir = new File(outputDir, "resource_helper")
9 | packageDir.mkdirs()
10 |
11 | String classContent = buildClass(relativePath, inputDir, 0)
12 | File resourcesClass = new File(packageDir, "Resources.java")
13 | resourcesClass.write(classContent)
14 | }
15 | }
16 |
17 | def task = tasks.findByPath("compileKotlin")
18 | if (task != null)
19 | task.dependsOn('compileResourcesClass')
20 | else
21 | tasks.getByName("compileJava").dependsOn("compileResourcesClass")
22 |
23 | static def buildClass(String relativePath, File file, int tabCount = 0) {
24 | def tabs = ""
25 | tabCount.times { tabs += "\t" }
26 | def fieldName = fileToFieldName(file)
27 |
28 | if (!file.isDirectory()) {
29 | def value = file.path.replace(relativePath, "")
30 | return "${tabs}public static String $fieldName = \"${value}\";\n"
31 | }
32 |
33 | String classText = ""
34 | if (tabCount == 0) {
35 | fieldName = fieldName.capitalize()
36 | classText += "package resource_helper;\n"
37 | classText += "${tabs}public class ${fieldName.capitalize()} {\n"
38 | } else {
39 | classText += "${tabs}public static class $fieldName {\n"
40 | }
41 | classText += file.listFiles().collect { buildClass(relativePath, it, tabCount + 1) }.join()
42 | classText += "${tabs}}\n"
43 | return classText
44 | }
45 |
46 | static def fileToFieldName(File file) {
47 | String fieldName = file.name
48 | .split("[^a-zA-Z0-9]+")
49 | .collect { it.capitalize() }
50 | .join()
51 | .uncapitalize()
52 | .toString()
53 |
54 | if (fieldName.matches(/^[^a-zA-Z_].*/))
55 | return "_" + fieldName
56 | return fieldName
57 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/brunodles/anime-watcher/tree/master)
2 | [](https://travis-ci.org/brunodles/anime-watcher)
3 | # Anime Watcher
4 | An video player / browser to navigate through unsafe pages.
5 |
6 | Initially this repo was to make a AnimePlayer that grab data from anime pages.
7 | The idea was to provide a way to reach information and video url.
8 | Now the idea changed to decode any page, avoiding ads.
9 |
10 | ## Why
11 | Many pages have **tons of ads** before the user be able to reach out the content.
12 | Some of these pages are stealing content from the others, so the idea is to let any one reach the content avoiding ads.
13 | Then we won't send money ( through ads) to those pages.
14 |
15 | ### Objectives
16 | * Decode Page contents
17 | * Avoid ads
18 | * Provide a good experience when watch videos from pages
19 | * Provide a offline support for online videos
20 | * Fight piracy?
21 |
22 | ## About the project
23 |
24 | This project is divided into these modules:
25 | * [Explorer](/explorer) : a module to decode pages.
26 | * [Server Spark](/server-spark) : a web server that uses Explorer modules.
27 | * [Cli](/cli) : simple command to interact with Explorer on terminal.
28 | * [App](app) : an android app that uses communicates with Server-Spark to
29 | play decoded pages.
30 | * [Cloud Functions](/cloudfunctions) : firebase cloud functions, work as a middleware.
31 | * [Firebase Structure](/Firebase.md) : how data are stored.
32 | * [Decoders](/decoders) : were used for decoders, some pages still there.
33 | * [Cache](/cache) : just a shared cache folder.
34 | * [gradle](/gradle) : useful script, some will be extracted to a plugin.
35 |
36 | ## Contributing
37 | We have a lot of issues and ideas to implement.
38 | You can help us implementing new decodes, fixing bugs, suggesting new pages and features.
39 | Just create a issue.
40 |
41 | If you're doing one of those issues pleas leave a comment so we will wait for you MR.
42 |
--------------------------------------------------------------------------------
/Firebase.md:
--------------------------------------------------------------------------------
1 | # Firebase Structure
2 |
3 | ```json
4 | {
5 | "//" : "this is the environment name, all data related to this environement will be kept inside it.",
6 | "{{environment}}" : {
7 | "users" : {
8 | "{{user.id}}" : {
9 | "//" : "May change in the future to keep player position",
10 | "history" : {
11 | "{{video.id}}" : "{{video.url}}"
12 | }
13 | }
14 | },
15 | "videos": {
16 | "{{video.url.toKey}}" : {
17 | "animeName" : "{{video.animeName}}",
18 | "description" : "{{video.description}}",
19 | "//" : "Video Thumbnail",
20 | "image" : "{{video.image}}",
21 | "link" : "{{video.link}}",
22 | "number" : {{video.number}},
23 | "//" : "the url of video to be used for players",
24 | "video" : "{{video.video}}",
25 | "nextEpisodes" : {
26 | "//" : "May repeat or be missing",
27 | "{{index}}" : {
28 | "animeName" : "{{video.animeName}}",
29 | "description" : "{{video.description}}",
30 | "image" : "{{video.image}}",
31 | "link" : "{{video.link}}",
32 | "number" : {{video.number}},
33 | "//" : "Video will probably be missing",
34 | "video" : "{{video.video}}",
35 | }
36 | }
37 | }
38 | }
39 | },
40 | "//" : "tracks all pages that works with the app, may be used to disable it.",
41 | "pages" : {
42 | "//" : "The name in key form (just a valid form for firebase)",
43 | "{{name.toKey}}" : {
44 | "enabled" : "{{true|false}}",
45 | "url" : "{{page.url}}"
46 | }
47 | },
48 | "//" : "servers to redirect, may be used to disable.",
49 | "servers" : {
50 | "{{page.url.toKey}}" : {
51 | "enabled" : "{{true|false}}",
52 | "url" : "{{page.url}}"
53 | }
54 | }
55 | }
56 | ```
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/search/SearchController.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.search
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import brunodles.animewatcher.BuildConfig
6 | import brunodles.animewatcher.explorer.Episode
7 | import brunodles.animewatcher.persistence.Firebase
8 | import brunodles.animewatcher.player.EpisodeController
9 | import brunodles.rxfirebase.typedChildObserver
10 | import io.reactivex.android.schedulers.AndroidSchedulers
11 | import io.reactivex.rxkotlin.subscribeBy
12 | import io.reactivex.schedulers.Schedulers
13 | import java.net.URLEncoder
14 |
15 | internal class SearchController(val context: Context) {
16 | companion object {
17 | private const val TAG = "SearchController"
18 | private const val GOOGLE_SEARCH = "http://google.com/search?q="
19 | }
20 |
21 | val episodeController: EpisodeController by lazy { EpisodeController(context) }
22 |
23 | fun buildUrl(query: String) = GOOGLE_SEARCH + URLEncoder.encode(
24 | "$query ${BuildConfig.GOGOLE_QUERY}",
25 | "UTF-8"
26 | )
27 |
28 | fun addSearch(query: String) {
29 | Firebase.addToSearchHistory(query)
30 | }
31 |
32 | fun searchHistory() = Firebase.searchHistory()
33 | .limitToLast(100)
34 | .orderByKey()
35 | .typedChildObserver(String::class.java)
36 | .subscribeOn(Schedulers.io())
37 |
38 | fun onUrl(
39 | url: String?,
40 | playerStarter: (Episode) -> Unit,
41 | urlLoader: (String) -> Unit
42 | ): Boolean {
43 | if (url == null)
44 | return false
45 | if (url.contains("google.com")) {
46 | Log.d(TAG, "shouldOverrideUrl: containsGoogle, using default")
47 | return false
48 | }
49 |
50 | episodeController.findVideoOn(url)
51 | .toMaybe()
52 | .observeOn(AndroidSchedulers.mainThread())
53 | .subscribeBy(
54 | onSuccess = {
55 | playerStarter.invoke(it)
56 | },
57 | onError = {
58 | urlLoader(url)
59 | }
60 | )
61 | return true
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/rxfirebase/SingleValueEvent.kt:
--------------------------------------------------------------------------------
1 | package brunodles.rxfirebase
2 |
3 | import com.google.firebase.database.DataSnapshot
4 | import com.google.firebase.database.DatabaseError
5 | import com.google.firebase.database.DatabaseReference
6 | import com.google.firebase.database.Query
7 | import com.google.firebase.database.ValueEventListener
8 | import io.reactivex.Observable
9 | import io.reactivex.ObservableEmitter
10 | import io.reactivex.Single
11 | import io.reactivex.SingleEmitter
12 |
13 | fun DatabaseReference.singleObservable(valueClass: Class): Single {
14 | return Single.create { emitter ->
15 | this.addListenerForSingleValueEvent(SingleEventParseListener(emitter, valueClass))
16 | }
17 | }
18 |
19 | fun DatabaseReference.singleObservable(): Observable {
20 | return Observable.create { emitter ->
21 | this.addListenerForSingleValueEvent(SingleEventListener(emitter))
22 | }
23 | }
24 |
25 | fun Query.singleObservable(valueClass: Class): Single {
26 | return Single.create { emitter ->
27 | this.addListenerForSingleValueEvent(SingleEventParseListener(emitter, valueClass))
28 | }
29 | }
30 |
31 | private class SingleEventListener(val emitter: ObservableEmitter) :
32 | ValueEventListener {
33 |
34 | override fun onCancelled(p0: DatabaseError) {
35 | emitter.onError(p0.toException())
36 | }
37 |
38 | override fun onDataChange(p0: DataSnapshot) {
39 | emitter.onNext(p0)
40 | emitter.onComplete()
41 | }
42 | }
43 |
44 | private class SingleEventParseListener(
45 | val emitter: SingleEmitter,
46 | val valueClass: Class
47 | ) : ValueEventListener {
48 |
49 | override fun onCancelled(p0: DatabaseError) {
50 | emitter.onError(p0.toException())
51 | }
52 |
53 | override fun onDataChange(p0: DataSnapshot) {
54 | if (!p0.exists()) {
55 | emitter.onError(IllegalArgumentException("Value not found on requested reference."))
56 | return
57 | }
58 | val value = p0.getValue(valueClass)
59 | if (value != null)
60 | emitter.onSuccess(value)
61 | else
62 | emitter.onError(NullPointerException())
63 | }
64 | }
--------------------------------------------------------------------------------
/server-ktor/src/main/java/com/brunodles/animewatcher/serverktor/Application.kt:
--------------------------------------------------------------------------------
1 | package com.brunodles.animewatcher.serverktor
2 |
3 | import brunodles.animewatcher.decoders.UrlChecker
4 | import brunodles.animewatcher.explorer.Episode
5 | import com.google.gson.Gson
6 | import io.ktor.application.Application
7 | import io.ktor.application.ApplicationCallPipeline
8 | import io.ktor.application.call
9 | import io.ktor.application.install
10 | import io.ktor.features.CallLogging
11 | import io.ktor.features.Compression
12 | import io.ktor.features.StatusPages
13 | import io.ktor.http.HttpStatusCode
14 | import io.ktor.response.respond
15 | import io.ktor.routing.Routing
16 | import io.ktor.routing.get
17 | import io.ktor.routing.route
18 | import io.ktor.routing.routing
19 | import java.nio.file.ClosedFileSystemException
20 |
21 | typealias VideoChecker = (String) -> Episode?
22 |
23 | private val gson = Gson()
24 |
25 | fun Application.animewatcher(videoChecker: VideoChecker = UrlChecker::videoInfo) {
26 | installs()
27 | routing {
28 | v1Routing(videoChecker)
29 | route("{...}") {
30 | handle {
31 | call.respond(HttpStatusCode.NotFound)
32 | }
33 | }
34 | }
35 | }
36 |
37 | private fun Application.installs() {
38 | install(Compression)
39 | install(CallLogging)
40 | install(StatusPages) {
41 | status(HttpStatusCode.NotFound) { call.respond(HttpStatusCode.NotFound, "Page not found.") }
42 | }
43 | }
44 |
45 | private fun Routing.v1Routing(videoChecker: VideoChecker) {
46 | route("v1") {
47 | get("/decoder") {
48 | val url = call.parameters["url"]
49 | if (url == null) {
50 | call.respond(HttpStatusCode.BadRequest, "Requested url was invalid.")
51 | return@get
52 | }
53 | val episode: Episode? = videoChecker.invoke(url)
54 |
55 | if (episode == null)
56 | call.respond(HttpStatusCode.BadGateway, "Invalid response from anime page")
57 | else
58 | call.respond(HttpStatusCode.OK, gson.toJson(episode))
59 | }
60 | }
61 | intercept(ApplicationCallPipeline.Fallback) {
62 | throw ClosedFileSystemException()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/rxfirebase/TypedEvent.kt:
--------------------------------------------------------------------------------
1 | package brunodles.rxfirebase
2 |
3 | import com.google.firebase.database.ChildEventListener
4 | import com.google.firebase.database.DataSnapshot
5 | import com.google.firebase.database.DatabaseError
6 | import com.google.firebase.database.DatabaseReference
7 | import com.google.firebase.database.Query
8 | import io.reactivex.Observable
9 | import io.reactivex.ObservableEmitter
10 |
11 | enum class EventType {
12 | MOVED, CHANGED, ADDED, REMOVED
13 | }
14 |
15 | class TypedEvent(val event: EventType, val element: ELEMENT, val key: String)
16 |
17 | fun DatabaseReference.typedChildObserver(valueClass: Class): Observable> {
18 | return Observable.create { emitter ->
19 | this.addChildEventListener(TypedEventParseListener(emitter, valueClass))
20 | }
21 | }
22 |
23 | fun Query.typedChildObserver(valueClass: Class): Observable> {
24 | return Observable.create { emitter ->
25 | this.addChildEventListener(TypedEventParseListener(emitter, valueClass))
26 | }
27 | }
28 |
29 | private class TypedEventParseListener(
30 | val emitter: ObservableEmitter>,
31 | val valueClass: Class
32 | ) :
33 | ChildEventListener {
34 | override fun onCancelled(p0: DatabaseError) {
35 | emitter.onError(p0.toException())
36 | }
37 |
38 | override fun onChildMoved(p0: DataSnapshot, p1: String?) = sendEvent(p0, EventType.MOVED)
39 |
40 | override fun onChildChanged(p0: DataSnapshot, p1: String?) = sendEvent(p0, EventType.CHANGED)
41 |
42 | override fun onChildAdded(p0: DataSnapshot, p1: String?) = sendEvent(p0, EventType.ADDED)
43 |
44 | override fun onChildRemoved(p0: DataSnapshot) = sendEvent(p0, EventType.REMOVED)
45 |
46 | private fun sendEvent(p0: DataSnapshot, type: EventType) {
47 | if (!p0.exists()) {
48 | emitter.onError(IllegalArgumentException("Value not found on requested reference."))
49 | return
50 | }
51 | try {
52 | val value = p0.getValue(valueClass)
53 | if (value != null) {
54 | emitter.onNext(TypedEvent(type, value, p0.key!!))
55 | }
56 | } catch (e: Exception) {
57 | emitter.onError(e)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/components/TextInputAutoCompleteTextView.kt:
--------------------------------------------------------------------------------
1 | package brunodles.components
2 |
3 | import android.content.Context
4 | import android.graphics.Rect
5 | import android.support.design.widget.TextInputLayout
6 | import android.support.v7.widget.AppCompatAutoCompleteTextView
7 | import android.util.AttributeSet
8 | import android.view.inputmethod.EditorInfo
9 | import android.view.inputmethod.InputConnection
10 |
11 | // Reference found on: https://stackoverflow.com/questions/39431378/textinputlayout-and-autocompletetextview/41864063#41864063
12 | // Based on code TextInputEditText
13 | /**
14 | * An implementation of @class AppCompatAutoCompleteTextView
15 | * With override on @see #onCreateInputConnection
16 | * Based on official implementation of @class TextInputEditText
17 | */
18 | class TextInputAutoCompleteTextView : AppCompatAutoCompleteTextView {
19 |
20 | constructor(context: Context) : super(context)
21 |
22 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
23 |
24 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
25 | context,
26 | attrs,
27 | defStyleAttr
28 | )
29 |
30 | /*
31 | Auto performFilter
32 | Thanks to CommonsWare on: https://stackoverflow.com/a/2126852/1622925
33 | */
34 | override fun enoughToFilter(): Boolean = true
35 |
36 | /*
37 | When receive focus performFiltering, so the dropdown pops up.
38 | Thanks to David Vávra on: https://stackoverflow.com/a/5783983/1622925
39 | */
40 | override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
41 | super.onFocusChanged(focused, direction, previouslyFocusedRect)
42 | if (focused && adapter != null)
43 | performFiltering(text, 0)
44 | }
45 |
46 | override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
47 | val ic = super.onCreateInputConnection(outAttrs)
48 | if (ic != null && outAttrs.hintText == null) {
49 | // If we don't have a hint and our parent is a TextInputLayout, use it's hint for the
50 | // EditorInfo. This allows us to display a hint in 'extract mode'.
51 | val parent = parent
52 | if (parent is TextInputLayout) {
53 | outAttrs.hintText = parent.hint
54 | }
55 | }
56 | return ic
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/RedirectFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | internal class RedirectFetcher(
6 | val nestedFetcher: UrlFetcher,
7 | val maxRedirects: Int = DEFAULT_MAX_REDIRECTS
8 | ) : UrlFetcher {
9 |
10 | companion object {
11 | private const val DEFAULT_MAX_REDIRECTS = 2
12 | private val JS_REDIRECT_LOCATION_REGEX =
13 | Regex("(?:window|self|top)?location(?:\\.href|assign|replace)?\\s*=\\s*[\"'](.*?)[\"'];")
14 | private val JS_REDIRECT_NAVIGATE_REGEX = Regex("navigateTo\\(.*?[\"'](https?.*?)[\"']\\)")
15 | }
16 |
17 | override fun get(url: String): Document = get(url, 0)
18 |
19 | private fun get(url: String, level: Int): Document {
20 | val document = nestedFetcher.get(url)
21 | if (level >= DEFAULT_MAX_REDIRECTS) return document
22 | return followRedirect(document, level + 1)
23 | }
24 |
25 | private fun followRedirect(document: Document, level: Int): Document {
26 | val redirectNoScript = redirecNoScript(document)
27 | if (redirectNoScript != null && redirectNoScript.isNotBlank()) {
28 | Logger.log { "follow redirectNoScript to $redirectNoScript" }
29 | return get(redirectNoScript, level)
30 | }
31 |
32 | val redirectJs = redirectJs(document)
33 | if (redirectJs != null && redirectJs.isNotBlank()) {
34 | Logger.log { "follow redirectJs to $redirectJs" }
35 | return get(redirectJs, level)
36 | }
37 |
38 | return document
39 | }
40 |
41 | private fun redirecNoScript(document: Document): String? {
42 | val content = document.head()?.select("noscript meta[http-equiv=refresh]")
43 | ?.content()
44 | val split = content?.split("'")
45 | if (split?.size == 3)
46 | return split[1]
47 | return null
48 | }
49 |
50 | private fun redirectJs(document: Document): String? {
51 | val text = document.head().html()
52 | val navigateMatcher = JS_REDIRECT_NAVIGATE_REGEX.find(text)
53 | if (navigateMatcher?.groups?.size ?: 0 > 1)
54 | return navigateMatcher?.groupValues?.get(1)
55 | val locationMatcher = JS_REDIRECT_LOCATION_REGEX.find(text)
56 | if (locationMatcher?.groups?.size ?: 0 > 1)
57 | return locationMatcher?.groupValues?.get(1)
58 | return null
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/AnitubeBrFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.ToInt
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.animewatcher.explorer.PageParser
7 | import com.brunodles.alchemist.collectors.AttrCollector
8 | import com.brunodles.alchemist.collectors.TextCollector
9 | import com.brunodles.alchemist.nested.Nested
10 | import com.brunodles.alchemist.selector.Selector
11 | import com.brunodles.alchemist.stringformat.StringFormat
12 |
13 | object AnitubeBrFactory : PageParser {
14 |
15 | private val EPISODE_URL_REGEX = Regex("(?:https?://)?(?:www\\.)?anitubebr\\.com/vd/\\d+.*")
16 |
17 | override fun isEpisode(url: String): Boolean = url.matches(EPISODE_URL_REGEX)
18 |
19 | override fun episode(url: String): Episode {
20 | val currentEpisode = AlchemistFactory.alchemist.parseUrl(url, CurrentEpisode::class.java)
21 | with(currentEpisode) {
22 | return Episode(description(), number(), animeName(), image(), null, url,
23 | nextEpisodes()?.let {
24 | listOf(Episode(it.title(), number() + 1, animeName(), link = it.link()))
25 | } ?: emptyList())
26 | }
27 | }
28 |
29 | interface CurrentEpisode {
30 |
31 | @Selector("[itemprop=description]")
32 | @AttrCollector("content")
33 | fun description(): String
34 |
35 | @Selector("[itemprop=name]")
36 | @AttrCollector("content")
37 | @Regexp("(\\d+)")
38 | @ToInt
39 | fun number(): Int
40 |
41 | @Selector("[itemprop=video] [itemprop=name]")
42 | @AttrCollector("content")
43 | @Regexp("^([a-zA-Z\\s]+?)(?:[\\s\\d]+)\$")
44 | fun animeName(): String?
45 |
46 | @Selector("video source")
47 | @AttrCollector("src")
48 | fun video(): String?
49 |
50 | @Selector("[itemprop=thumbnailUrl]")
51 | @AttrCollector("content")
52 | fun image(): String?
53 |
54 | @Selector(".idonproximo a")
55 | @Nested
56 | fun nextEpisodes(): NextEpisode?
57 | }
58 |
59 | interface NextEpisode {
60 | @Selector("a")
61 | @AttrCollector("href")
62 | @StringFormat("https://anitubebr.com%s")
63 | fun link(): String
64 |
65 | @Selector("a")
66 | @TextCollector
67 | fun title(): String
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/home/HomeActivity.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.home
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.databinding.DataBindingUtil
6 | import android.os.Bundle
7 | import android.support.v4.app.Fragment
8 | import android.support.v7.app.AppCompatActivity
9 | import android.util.Log
10 | import brunodles.animewatcher.R
11 | import brunodles.animewatcher.databinding.ActivityHomeBinding
12 | import brunodles.animewatcher.explorer.Episode
13 | import brunodles.animewatcher.history.HistoryFragment
14 | import brunodles.animewatcher.nextepisodes.NextEpisodeFragment
15 | import brunodles.animewatcher.player.PlayerActivity
16 | import brunodles.animewatcher.search.SearchFragment
17 | import io.reactivex.disposables.CompositeDisposable
18 |
19 | class HomeActivity : AppCompatActivity() {
20 |
21 | companion object {
22 | val TAG = "HomeActivity"
23 |
24 | fun newIntent(context: Context): Intent? =
25 | Intent(context, HomeActivity::class.java)
26 | }
27 |
28 | private lateinit var binding: ActivityHomeBinding
29 | private val disposable = CompositeDisposable()
30 | private val historyFragment by lazy { HistoryFragment() }
31 | private val nextEpisodeFragment by lazy { NextEpisodeFragment() }
32 | private val searchFragment by lazy { SearchFragment() }
33 |
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
37 | binding.bottomNavigation.setOnNavigationItemSelectedListener {
38 | Log.d(TAG, "onNavigationItemSelected ${resources.getResourceName(it.itemId)}")
39 | changeFragment(fragmentFor(it.itemId))
40 | true
41 | }
42 | binding.bottomNavigation.selectedItemId = R.id.history
43 | }
44 |
45 | private fun changeFragment(fragment: Fragment) {
46 | supportFragmentManager.beginTransaction()
47 | .replace(R.id.container, fragment, "container")
48 | .commit()
49 | }
50 |
51 | private fun fragmentFor(itemId: Int): Fragment = when (itemId) {
52 | R.id.history -> historyFragment
53 | R.id.nextEpisodes -> nextEpisodeFragment
54 | R.id.search -> searchFragment
55 | else -> historyFragment
56 | }
57 |
58 | override fun onDestroy() {
59 | super.onDestroy()
60 | disposable.clear()
61 | }
62 |
63 | fun onItemClick(episode: Episode) {
64 | startActivity(PlayerActivity.newIntent(this, episode))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/decoders/anitubex/src/main/kotlin/brunodles/anitubex/AnitubexFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.anitubex
2 |
3 | import brunodles.animewatcher.explorer.*
4 | import brunodles.urlfetcher.UrlFetcher
5 | import brunodles.urlfetcher.href
6 | import brunodles.urlfetcher.src
7 | import org.jsoup.nodes.Document
8 | import java.util.regex.Pattern
9 |
10 | object AnitubexFactory : PageParser {
11 |
12 | private val EPISODE_URL_REGEX = Regex("anitubex.com/.*?\\d+")
13 | private val HREF_REGEX = Pattern.compile("href:\\s?\"(.*?)\",")
14 | private val SPACES = Pattern.compile("\\s")
15 |
16 | override fun isEpisode(url: String): Boolean = url.contains(EPISODE_URL_REGEX)
17 |
18 | override fun episode(url: String): Episode {
19 | val doc = UrlFetcher.fetchUrl(url)
20 |
21 | val text = doc.select(".panel-heading h1").text()
22 | var iframeLink = doc.select(".tab-pane iframe").src()
23 | val number = extractNumberFromText(text)
24 | val nextEpisodes = findNextEpisodes(doc, number + 1)
25 |
26 | var iframe = UrlFetcher.fetchUrl(iframeLink)
27 | do {
28 | iframeLink = iframe.select("iframe").src()
29 | if (iframeLink.isNullOrEmpty()) {
30 | val matcher = HREF_REGEX.matcher(iframe.html())
31 | if (matcher.find())
32 |
33 | return Episode(
34 | description = text,
35 | number = number,
36 | video = matcher.group(1),
37 | link = url,
38 | nextEpisodes = nextEpisodes)
39 | break
40 | }
41 | iframe = UrlFetcher.fetchUrl(iframeLink)
42 | } while (iframeLink != null)
43 | val videoUrl = iframe.select("#advideox").src()
44 |
45 | return Episode(
46 | description = text,
47 | number = number,
48 | video = videoUrl,
49 | link = url,
50 | nextEpisodes = nextEpisodes)
51 | }
52 |
53 | private fun extractNumberFromText(text: String): Int {
54 | val split = text.split(SPACES)
55 | val last = split.last()
56 | return last.toIntOrNull() ?: 0
57 | }
58 |
59 | private fun findNextEpisodes(doc: Document, number: Int): List {
60 | return doc.select(".btn-nav-episodios.next")
61 | .map {
62 | Episode(description = it.text(),
63 | number = it.href().split("-").last().toIntOrNull() ?: number,
64 | link = it.href())
65 | }.toList()
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/rxfirebase/ChildAddedEvent.kt:
--------------------------------------------------------------------------------
1 | package brunodles.rxfirebase
2 |
3 | import com.google.firebase.database.ChildEventListener
4 | import com.google.firebase.database.DataSnapshot
5 | import com.google.firebase.database.DatabaseError
6 | import com.google.firebase.database.DatabaseReference
7 | import com.google.firebase.database.Query
8 | import io.reactivex.Observable
9 | import io.reactivex.ObservableEmitter
10 |
11 | fun DatabaseReference.observableChildAdded(): Observable {
12 | return Observable.create { emitter ->
13 | this.addChildEventListener(ChildAddedListener(emitter))
14 | }
15 | }
16 |
17 | fun DatabaseReference.observableChildAdded(valueClass: Class): Observable {
18 | return Observable.create { emitter ->
19 | this.addChildEventListener(ChildAddedParseListener(emitter, valueClass))
20 | }
21 | }
22 |
23 | fun Query.observableChildAdded(valueClass: Class): Observable {
24 | return Observable.create { emitter ->
25 | this.addChildEventListener(ChildAddedParseListener(emitter, valueClass))
26 | }
27 | }
28 |
29 | private class ChildAddedListener(val emitter: ObservableEmitter) :
30 | ChildEventListener {
31 | override fun onChildMoved(p0: DataSnapshot, p1: String?) {
32 | }
33 |
34 | override fun onChildChanged(p0: DataSnapshot, p1: String?) {
35 | }
36 |
37 | override fun onChildAdded(p0: DataSnapshot, p1: String?) {
38 | emitter.onNext(p0)
39 | }
40 |
41 | override fun onChildRemoved(p0: DataSnapshot) {
42 | }
43 |
44 | override fun onCancelled(p0: DatabaseError) {
45 | emitter.onError(p0.toException())
46 | }
47 | }
48 |
49 | private class ChildAddedParseListener(
50 | val emitter: ObservableEmitter,
51 | val valueClass: Class
52 | ) : ChildEventListener {
53 | override fun onChildMoved(p0: DataSnapshot, p1: String?) {
54 | }
55 |
56 | override fun onChildChanged(p0: DataSnapshot, p1: String?) {
57 | }
58 |
59 | override fun onChildAdded(p0: DataSnapshot, p1: String?) {
60 | if (!p0.exists()) {
61 | emitter.onError(IllegalArgumentException("Value not found on requested reference."))
62 | return
63 | }
64 | try {
65 | val value = p0.getValue(valueClass)
66 | if (value != null)
67 | emitter.onNext(value)
68 | } catch (e: Exception) {
69 | emitter.onError(e)
70 | }
71 | }
72 |
73 | override fun onChildRemoved(p0: DataSnapshot) {
74 | }
75 |
76 | override fun onCancelled(p0: DatabaseError) {
77 | emitter.onError(p0.toException())
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
22 |
23 |
34 |
35 |
36 |
37 |
49 |
50 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/AnitubeSiteFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.ToInt
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.animewatcher.explorer.PageParser
7 | import com.brunodles.alchemist.collectors.AttrCollector
8 | import com.brunodles.alchemist.collectors.TextCollector
9 | import com.brunodles.alchemist.nested.Nested
10 | import com.brunodles.alchemist.selector.Selector
11 |
12 | object AnitubeSiteFactory : PageParser {
13 |
14 | private val EPISODE_URL_REGEX = Regex("https?://www\\.anitube\\.site/\\d+.*")
15 |
16 | override fun isEpisode(url: String): Boolean = url.matches(EPISODE_URL_REGEX)
17 |
18 | override fun episode(url: String): Episode {
19 | val currentEpisode = AlchemistFactory.alchemist.parseUrl(url, CurrentEpisode::class.java)
20 | with(currentEpisode) {
21 | val description = description()
22 | val number = number()
23 | val animeName = animeName()
24 | val image = image()
25 | val video = video()
26 | return Episode(description, number, animeName, image, video, url,
27 | nextEpisode()?.let {
28 | listOf(Episode(it.title(), it.number(), animeName, link = it.link()))
29 | } ?: emptyList() )
30 | }
31 | }
32 |
33 | interface CurrentEpisode {
34 |
35 | @Selector("section#descricao p:last-child")
36 | @TextCollector
37 | @Regexp("^(?:.*?[–-]+\\s?)+(.*?)\$")
38 | fun description(): String
39 |
40 | @Selector("#descricao p")
41 | @TextCollector
42 | @Regexp("(\\d+)")
43 | @ToInt
44 | fun number(): Int
45 |
46 | @Selector("title")
47 | @TextCollector
48 | @Regexp("^([\\w\\s]+)[\\s-]")
49 | fun animeName(): String?
50 |
51 | @Selector("video source")
52 | @AttrCollector("src")
53 | fun video(): String?
54 |
55 | @Selector("video")
56 | @AttrCollector("poster")
57 | fun image(): String?
58 |
59 | @Selector("#baixo #right a")
60 | @Nested
61 | fun nextEpisode(): NextEpisode?
62 | }
63 |
64 | interface NextEpisode {
65 | @Selector("a")
66 | @AttrCollector("href")
67 | fun link(): String
68 |
69 | @Selector("a")
70 | @AttrCollector("title")
71 | @Regexp("^(?:.*?[–-]+\\s?)+(.*?)\$")
72 | fun title(): String
73 |
74 | @Selector("a")
75 | @AttrCollector("title")
76 | @Regexp("(\\d+)")
77 | @ToInt
78 | fun number(): Int
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto start
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto start
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :start
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/nextepisodes/EpisodeAdapter.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.nextepisodes
2 |
3 | import android.annotation.SuppressLint
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import brunodles.animewatcher.ImageLoader
8 | import brunodles.animewatcher.R
9 | import brunodles.animewatcher.databinding.ItemEpisodeBinding
10 | import brunodles.animewatcher.explorer.Episode
11 | import brunodles.animewatcher.history.OnItemClick
12 |
13 | class EpisodeAdapter : RecyclerView.Adapter() {
14 |
15 | private val list = mutableListOf()
16 | private var layoutInflater: LayoutInflater? = null
17 | private var onEpisodeClickListener: OnItemClick? = null
18 | private var internalEpisodeClickListener: OnItemClick = {
19 | onEpisodeClickListener?.invoke(it)
20 | }
21 |
22 | fun setEpisodeClickListener(listener: OnItemClick?) {
23 | onEpisodeClickListener = listener
24 | }
25 |
26 | override fun getItemCount(): Int = list.count()
27 |
28 | override fun onBindViewHolder(holder: EpisodeHolder, position: Int) {
29 | holder.let {
30 | it.onBind(list[position])
31 | it.clickListener = internalEpisodeClickListener
32 | }
33 | }
34 |
35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeHolder {
36 | val context = parent.context
37 | if (layoutInflater == null)
38 | layoutInflater = LayoutInflater.from(context)
39 | return EpisodeHolder(
40 | ItemEpisodeBinding.inflate(
41 | layoutInflater!!,
42 | parent,
43 | false
44 | )
45 | )
46 | }
47 |
48 | class EpisodeHolder(val binder: ItemEpisodeBinding) : RecyclerView.ViewHolder(binder.root) {
49 |
50 | var clickListener: OnItemClick? = null
51 |
52 | @SuppressLint("SetTextI18n")
53 | fun onBind(item: Episode) {
54 | binder.root.setOnClickListener { clickListener?.invoke(item) }
55 | if (item.number > 0)
56 | binder.description.text = "${item.number} - ${item.description}"
57 | else
58 | binder.description.text = item.description
59 | binder.title.text = item.animeName
60 | binder.image.setImageResource(R.drawable.img_loading)
61 | ImageLoader.loadImageInto(item.image, binder.image)
62 | }
63 | }
64 |
65 | fun clear() {
66 | list.clear()
67 | notifyDataSetChanged()
68 | }
69 |
70 | fun add(it: Episode) {
71 | val position = list.size
72 | if (list.add(it))
73 | notifyItemInserted(position)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/collection/ArrayWithKeys.kt:
--------------------------------------------------------------------------------
1 | package brunodles.collection
2 |
3 | import android.support.v4.util.ArrayMap
4 |
5 | class ArrayWithKeys : MutableCollection
- {
6 |
7 | private val list: ArrayList
- = ArrayList()
8 | private val keyMap = ArrayMap()
9 |
10 | override var size: Int = 0
11 | get() = list.size
12 |
13 | override fun isEmpty() = list.isEmpty()
14 |
15 | override fun clear() {
16 | list.clear()
17 | keyMap.clear()
18 | }
19 |
20 | override fun addAll(elements: Collection
- ) = list.addAll(elements)
21 |
22 | override fun add(element: ITEM) = add(element, null) >= 0
23 |
24 | /**
25 | * @return index of the added item, -1 if invalid
26 | */
27 | fun add(element: ITEM, key: KEY? = null): Int {
28 | if (keyMap.contains(key))
29 | return -1
30 | synchronized(this) {
31 | val index = list.size
32 | list.add(index, element)
33 | keyMap.put(key, index)
34 | return index
35 | }
36 | }
37 |
38 | fun replace(element: ITEM, key: KEY): Int {
39 | if (!keyMap.contains(key))
40 | return -1
41 | val index = keyMap[key]!!
42 | list.removeAt(index)
43 | list.add(index, element)
44 | return index
45 | }
46 |
47 | override fun remove(element: ITEM): Boolean {
48 | val index = list.indexOf(element)
49 | if (index < 0) return false
50 | list.removeAt(index)
51 | val removeKeys = ArrayList()
52 | for (key in keyMap)
53 | if (index == key.value)
54 | removeKeys.add(key.key)
55 | for (key in removeKeys)
56 | keyMap.remove(key)
57 | return true
58 | }
59 |
60 | /**
61 | * @return the Index of the removed element
62 | */
63 | fun removeByKey(key: KEY): Int {
64 | val index = keyMap[key]
65 | if (index == null || index < 0)
66 | return -1
67 | list.removeAt(index)
68 | keyMap.remove(key)
69 | return index
70 | }
71 |
72 | override fun removeAll(elements: Collection
- ): Boolean {
73 | for (element in elements)
74 | if (!remove(element))
75 | return false
76 | return true
77 | }
78 |
79 | override fun retainAll(elements: Collection
- ): Boolean = list.retainAll(elements)
80 |
81 | override fun contains(element: ITEM): Boolean = list.contains(element)
82 |
83 | override fun containsAll(elements: Collection
- ): Boolean = list.containsAll(elements)
84 |
85 | override fun iterator(): MutableIterator
- = list.iterator()
86 |
87 | operator fun get(position: Int): ITEM = list[position]
88 |
89 | operator fun get(key: KEY): ITEM? = keyMap[key]?.let { list[it] }
90 |
91 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/cast/GoogleCaster.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.cast
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.support.v7.app.MediaRouteButton
6 | import android.util.Log
7 | import brunodles.animewatcher.explorer.Episode
8 | import com.google.android.gms.cast.MediaInfo
9 | import com.google.android.gms.cast.MediaLoadOptions
10 | import com.google.android.gms.cast.MediaMetadata
11 | import com.google.android.gms.cast.MediaStatus
12 | import com.google.android.gms.cast.framework.*
13 | import com.google.android.gms.cast.framework.media.RemoteMediaClient
14 | import com.google.android.gms.common.images.WebImage
15 |
16 | class GoogleCaster(context: Context, mediaRouteButton: MediaRouteButton?,
17 | val listener: DeviceConnectedListener? = null) : Caster {
18 |
19 | companion object {
20 | val TAG = "GoogleCaster"
21 | }
22 |
23 | val mSessionManager: SessionManager
24 |
25 | // val mSessionManagerListener: SessionManagerListener = SessionManagerListenerImpl()
26 | init {
27 | val applicationContext = context.applicationContext
28 | val castContext = CastContext.getSharedInstance(applicationContext)
29 | mSessionManager = castContext.sessionManager
30 | CastButtonFactory.setUpMediaRouteButton(applicationContext, mediaRouteButton)
31 | castContext.addCastStateListener { if (it == CastState.CONNECTED) listener?.invoke(this) }
32 | }
33 |
34 | fun castSession(): CastSession? = mSessionManager.currentCastSession
35 |
36 | override fun playRemote(currentEpisode: Episode, position: Long) {
37 | val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
38 |
39 | movieMetadata.putString(MediaMetadata.KEY_TITLE, currentEpisode.description)
40 | // movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
41 | currentEpisode.image?.let { movieMetadata.addImage(WebImage(Uri.parse(it))) }
42 |
43 | Log.d(TAG, "onCreate: currentEpisode.video = ${currentEpisode.video}")
44 | val mediaInfo = MediaInfo.Builder(currentEpisode.video)
45 | .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
46 | .setContentType("videos/mp4")
47 | .setMetadata(movieMetadata)
48 | // .setStreamDuration(mSelectedMedia.getDuration() * 1000)
49 | .build()
50 | val remoteMediaClient = castSession()?.remoteMediaClient
51 | val mediaLoadOptions = MediaLoadOptions.Builder()
52 | .setAutoplay(true)
53 | .setPlayPosition(position)
54 | .build()
55 | remoteMediaClient?.load(mediaInfo, mediaLoadOptions)
56 | remoteMediaClient?.play()
57 | }
58 |
59 | override fun isConnected(): Boolean = castSession()?.isConnected ?: false
60 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/urlfetcher/CacheFetcher.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import org.jsoup.Jsoup
4 | import org.jsoup.nodes.Document
5 | import java.io.File
6 | import java.util.regex.Pattern
7 |
8 | internal class CacheFetcher(private val nestedFetcher: UrlFetcher) : UrlFetcher {
9 |
10 | override fun get(url: String): Document {
11 | val key = urlToKey(url)
12 | if (isPageCached(key))
13 | return Jsoup.parse(loadPage(key))
14 | try {
15 | val document: Document = nestedFetcher.get(url)
16 | savePage(key, document.html())
17 | return document
18 | } catch (e: Throwable) {
19 | throw WrappedException(key, e)
20 | }
21 | }
22 |
23 | companion object {
24 |
25 | private val URL_PATTERN =
26 | Pattern.compile("^(?:.+?\\:\\/\\/)?(?:www\\.)?([\\w\\.\\-\\:]+)(?:[\\/\\?\\&](.+?))?\$")
27 | private val INVALID_TEXT_PATTERN = Regex("[^\\d\\w]+")
28 | private const val MAX_FILENAME_SIZE = 100
29 |
30 | private fun isPageCached(key: String): Boolean = file(key).exists()
31 |
32 | private fun savePage(key: String, page: String) {
33 | Logger.log { "savePage $key" }
34 | file(key).outputStream().bufferedWriter().use { it.write(page) }
35 | }
36 |
37 | private fun loadPage(key: String): String {
38 | Logger.log { "loadPage $key" }
39 | return file(key).inputStream().bufferedReader().use { it.readText() }
40 | }
41 |
42 | fun urlToKey(urlStr: String): String {
43 | val matcher = URL_PATTERN.matcher(urlStr)
44 | if (!matcher.find())
45 | throw IllegalArgumentException("Invalid Url parameter: \"$urlStr\"")
46 | val hostStr = matcher.group(1)
47 | .replace(Regex("[^\\d\\w.]"), "")
48 | if (matcher.groupCount() <= 1)
49 | return hostStr.fixed() + "/_index"
50 | val path = matcher.group(2) ?: "_index"
51 | return (hostStr + "/" + path.fixed())
52 | .max(MAX_FILENAME_SIZE)
53 | }
54 |
55 | private fun String?.fixed() = this?.replace(INVALID_TEXT_PATTERN, "") ?: ""
56 |
57 | private fun file(key: String): File {
58 | val dir = File(UrlFetcher.cacheDir, "cache")
59 | if (!dir.exists())
60 | dir.mkdirs()
61 | if (key.contains('/')) {
62 | val split = key.split('/')
63 | val hostDirName = split[0]
64 | val hostDir = File(dir, hostDirName)
65 | hostDir.mkdirs()
66 | return File(hostDir, split[1])
67 | }
68 | return File(dir, key)
69 | }
70 | }
71 |
72 | class WrappedException(cachePath: String, cause: Throwable) :
73 | RuntimeException("Failed to fetch cache using $cachePath", cause)
74 | }
75 |
--------------------------------------------------------------------------------
/decoders/animesproject/src/main/kotlin/brunodles/animesproject/AnimesProjectFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animesproject
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import brunodles.animewatcher.explorer.PageParser
5 | import brunodles.urlfetcher.UrlFetcher
6 | import org.jsoup.nodes.Document
7 | import java.util.regex.Pattern
8 |
9 | object AnimesProjectFactory : PageParser {
10 |
11 | private val EPISODE_URL_REGEX = Regex("animes.zlx.com.br/exibir/\\d+/\\d+")
12 | private val HOST = "http://animes.zlx.com.br"
13 | private val TITLE_PATTERN = Pattern.compile("(?:(.*?)\\s:\\s)?(.*?)\\s(\\d+)")
14 | private val VIDEO_URL_PATTERN = Pattern.compile("[\"']([lmh]q)[\"'](?:.*?)src[\"']\\s?:\\s?[\"'](.*?)[\"']")
15 | private val QUALITY_ORDER = arrayOf("hq", "mq", "lq")
16 |
17 | override fun isEpisode(url: String): Boolean = url.contains(EPISODE_URL_REGEX)
18 |
19 | override fun episode(url: String): Episode {
20 | val doc = UrlFetcher.fetchUrl(url)
21 | val iframe = doc.select("#player_frame").attr("src")
22 | val videoUrl = fixLink(findLink(iframe))
23 | val title = doc.select(".serie-pagina-subheader span").text()
24 | val (animeName, description, number) = splitTitle(title)
25 | return Episode(animeName = animeName,
26 | description = "$description $number",
27 | number = number,
28 | video = videoUrl,
29 | link = url,
30 | nextEpisodes = nextEpisodes(doc),
31 | temporaryVideoUrl = true)
32 | }
33 |
34 | private fun fixLink(link: String?): String? {
35 | if (link == null) return null
36 | return link.replace("\\/", "/")
37 | }
38 |
39 | private fun findLink(iframe: String?): String? {
40 | val html = UrlFetcher.fetchUrl(HOST + iframe).select("script").html()
41 | val videoMatcher = VIDEO_URL_PATTERN.matcher(html)
42 | val versions = HashMap()
43 | while (videoMatcher.find())
44 | versions.put(videoMatcher.group(1), videoMatcher.group(2))
45 | for (quality in QUALITY_ORDER)
46 | if (versions.containsKey(quality))
47 | return versions[quality]
48 | return null
49 | }
50 |
51 | private fun splitTitle(title: String?): Triple {
52 | val matcher = TITLE_PATTERN.matcher(title)
53 | matcher.find()
54 | val animeName = matcher.group(1) ?: null
55 | val description = matcher.group(2)
56 | val number = matcher.group(3).toIntOrNull() ?: 0
57 | return Triple(animeName, description, number)
58 | }
59 |
60 | fun nextEpisodes(doc: Document): List {
61 | return doc.select(".exibir-pagina-listagem a").map {
62 | val (_, _, number) = splitTitle(it.text())
63 | Episode(description = it.text(),
64 | number = number,
65 | link = HOST + it.attr("href"))
66 | }.toList().subList(3, 5)
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/ImageLoader.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.widget.ImageView
6 | import com.brunodles.googleimagesapi.ImagesApi
7 | import com.brunodles.googleimagesapi.PageFetcher
8 | import com.squareup.picasso.Picasso
9 | import io.reactivex.Observable
10 | import io.reactivex.schedulers.Schedulers
11 | import okhttp3.OkHttpClient
12 | import okhttp3.Request
13 | import java.io.IOException
14 | import java.lang.ref.WeakReference
15 |
16 | object ImageLoader {
17 | private var picassoSingleton: WeakReference? = null
18 |
19 | fun picasso(context: Context): Picasso {
20 | if (picassoSingleton?.get() == null)
21 | picassoSingleton = WeakReference(Picasso.Builder(context)
22 | .indicatorsEnabled(BuildConfig.DEBUG && BuildConfig.IMAGE_LOADER_LOGGGIN_ENABLED)
23 | .loggingEnabled(BuildConfig.IMAGE_LOADER_LOGGGIN_ENABLED)
24 | .defaultBitmapConfig(Bitmap.Config.RGB_565)
25 | .build())
26 | return picassoSingleton?.get()!!
27 | }
28 |
29 | fun loadImageInto(url: String?, image: ImageView) {
30 | if (url.isNullOrEmpty()) return
31 | picasso(image.context)
32 | .load(url)
33 | .placeholder(R.drawable.img_loading)
34 | .error(R.drawable.img_error)
35 | .into(image)
36 | }
37 |
38 | fun fetch(context: Context, url: String) =
39 | picasso(context)
40 | .load(url)
41 | .fetch()
42 |
43 | object imagesPageFetcher : PageFetcher {
44 | override fun fetchPage(url: String): String? {
45 | val client = OkHttpClient()
46 | val request = Request.Builder()
47 | .url(url)
48 | .get()
49 | .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36")
50 | .build()
51 | return try {
52 | val response = client.newCall(request).execute()
53 | response.body()?.let {
54 | val result = it.string()
55 | it.close()
56 | return result
57 | }
58 | } catch (e: IOException) {
59 | null
60 | }
61 | }
62 | }
63 |
64 | fun first(query: String): String? = search(query).first()
65 |
66 | fun search(query: String) =
67 | ImagesApi.queryBuilder(imagesPageFetcher)
68 | .query(query)
69 | .listImageUrls()
70 |
71 | fun searchObservable(query: String) =
72 | Observable.just(query)
73 | .subscribeOn(Schedulers.io())
74 | .map {
75 | ImagesApi.queryBuilder(imagesPageFetcher)
76 | .query(it)
77 | }
78 | }
--------------------------------------------------------------------------------
/server-ktor/src/test/kotlin/com/brunodles/animewatcher/serverktor/ApplicationKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.brunodles.animewatcher.serverktor
2 |
3 | import brunodles.animewatcher.explorer.Episode
4 | import io.ktor.application.Application
5 | import io.ktor.http.HttpMethod
6 | import io.ktor.http.HttpStatusCode
7 | import io.ktor.http.parametersOf
8 | import io.ktor.server.testing.TestApplicationEngine
9 | import io.ktor.server.testing.handleRequest
10 | import io.ktor.server.testing.withTestApplication
11 | import org.eclipse.jetty.util.UrlEncoded
12 | import org.junit.Assert.assertEquals
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import org.junit.runners.JUnit4
16 |
17 | @RunWith(JUnit4::class)
18 | class ApplicationKtTest {
19 |
20 | private val animeWatcherApplication: Application.() -> Unit = {
21 | animewatcher()
22 | }
23 |
24 | @Test
25 | fun whenDecode_withoutUrlParameter_shouldReturnBadRequest() = application {
26 | with(handleRequest(HttpMethod.Get, "/v1/decoder")) {
27 | assertEquals(HttpStatusCode.BadRequest, response.status())
28 | }
29 | }
30 |
31 | @Test
32 | fun whenDecode_withInvalidUrl_shouldReturnBadRequest() = application {
33 | val handleRequest = handleRequest(HttpMethod.Get, "/v1/decoder") {
34 | parametersOf("url", "kljsadlkjdsa")
35 | }
36 | with(handleRequest) {
37 | assertEquals(HttpStatusCode.BadRequest, response.status())
38 | }
39 | }
40 |
41 | @Test
42 | fun whenInvalidPath_shouldReturnNotFound() = application {
43 | with(handleRequest(HttpMethod.Get, "/123")) {
44 | assertEquals(HttpStatusCode.NotFound, response.status())
45 | }
46 | }
47 |
48 | @Test
49 | fun whenValidUrl_butServerCantBeReached_shouldReturnBadGateway() = withTestApplication({
50 | animewatcher { null }
51 | }) {
52 | val handleRequest = handleUrlRequest("http://validAnimeWebPage.com/linkToEpisode")
53 | with(handleRequest) {
54 | assertEquals(HttpStatusCode.BadGateway, response.status())
55 | }
56 | }
57 |
58 | @Test
59 | fun whenValidUrl_shouldReturnEpisodeInfo() = withTestApplication({
60 | animewatcher { Episode("Description", 10, "animeName", link = "") }
61 | }) {
62 | val handleRequest = handleUrlRequest("http://validAnimeWebPage.com/linkToEpisode")
63 | with(handleRequest) {
64 | assertEquals(HttpStatusCode.OK, response.status())
65 | assertEquals(
66 | "{\"description\":\"Description\",\"number\":10,\"animeName\":\"animeName\",\"link\":\"\",\"nextEpisodes\":[],\"temporaryVideoUrl\":false}",
67 | response.content
68 | )
69 | }
70 | }
71 |
72 | private fun application(func: TestApplicationEngine.() -> Unit) =
73 | withTestApplication(animeWatcherApplication, func)
74 |
75 | private fun TestApplicationEngine.handleUrlRequest(url: String) = handleRequest(
76 | HttpMethod.Get, "/v1/decoder?url=" + UrlEncoded.encodeString(url)
77 | )
78 | }
--------------------------------------------------------------------------------
/gradle/buildconfig.gradle:
--------------------------------------------------------------------------------
1 | class BuildConfigExtension {
2 | HashMap configFields = new HashMap<>()
3 | String targetPackage = ""
4 |
5 | void field(String type, String key, def value) {
6 | configFields.put(key, "public static final $type ${key.toUpperCase()} = $value;")
7 | }
8 |
9 | void mfield(String key, def value) {
10 | configFields.put(key, "public static final ${value.getClass().simpleName} ${key.toUpperCase()} = $value;")
11 | }
12 | }
13 |
14 | class BuildConfigTask extends DefaultTask {
15 |
16 | String group = "buildConfig"
17 | String description = "Generate BuildConfig class."
18 |
19 | BuildConfigTask() {
20 | outputs.upToDateWhen { false }
21 | finalizedBy("assemble")
22 | }
23 |
24 | @TaskAction
25 | def execute(IncrementalTaskInputs inputs) {
26 |
27 | def buildDir = project.getBuildDir()
28 | buildDir = new File(buildDir, "build-config")
29 | if (!project.buildconfig.targetPackage.isEmpty())
30 | buildDir = new File(buildDir, project.buildconfig.targetPackage.replaceAll("\\.", File.separator))
31 | buildDir.mkdirs()
32 |
33 | def outFile = new File(buildDir, "BuildConfig.java")
34 | outFile.createNewFile()
35 | StringBuilder content = new StringBuilder()
36 | if (!project.buildconfig.targetPackage.isEmpty())
37 | content.append("package ${project.buildconfig.targetPackage};\n\n")
38 | content.append("public final class BuildConfig {\n")
39 |
40 | project.buildconfig.configFields.each { f ->
41 | content.append(" ${f.value}\n")
42 | }
43 |
44 | project.fileTree(dir: "src", include: "**${File.separatorChar}buildconfig.properties").forEach { file ->
45 | println "BuildConfig file -> ${file.getAbsolutePath()}"
46 | def properties = new Properties()
47 | if (file.exists())
48 | properties.load(file.newReader())
49 |
50 | properties.each { property ->
51 | content.append(" ${property.value}\n")
52 | }
53 |
54 | }
55 | content.append("}\n")
56 | outFile.write(content.toString())
57 | }
58 | }
59 |
60 | class BuildConfigPlugin implements Plugin {
61 |
62 | @SuppressWarnings("GroovyUnusedDeclaration")
63 | public static final String VERSION = "0.1"
64 | private BuildConfigExtension extension
65 |
66 | @Override
67 | void apply(Project project) {
68 | project.sourceSets {
69 | main.java.srcDir "${project.buildDir}/build-config"
70 | }
71 | def task = project.tasks.create("generateBuildConfigClasses", BuildConfigTask.class)
72 | def compileKotlin = project.tasks.findByName("compileKotlin")
73 | if (compileKotlin != null)
74 | compileKotlin.dependsOn.add(task)
75 | else
76 | project.tasks.getByName("compileJava").dependsOn.add(task)
77 |
78 | extension = project.extensions.create("buildconfig", BuildConfigExtension)
79 | }
80 | }
81 |
82 | plugins.apply(BuildConfigPlugin.class)
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/TvCurseFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.ToInt
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.animewatcher.explorer.PageParser
7 | import com.brunodles.alchemist.collectors.AttrCollector
8 | import com.brunodles.alchemist.collectors.TextCollector
9 | import com.brunodles.alchemist.nested.Nested
10 | import com.brunodles.alchemist.regex.Regex
11 | import com.brunodles.alchemist.selector.Selector
12 |
13 | object TvCurseFactory : PageParser {
14 | private val URL_REGEX = kotlin.text.Regex("(?:tvcurse\\.com|animacurse\\.(?:tv|moe))\\/?\\?p=")
15 |
16 | override fun episode(url: String): Episode {
17 | val currentEpisode = AlchemistFactory.alchemist.parseUrl(url, CurrentEpisode::class.java)
18 | with(currentEpisode) {
19 | val nextEpisodes: List = try {
20 | nextEpisodes().toEpisode(animeName())
21 | } catch (e: Exception) {
22 | emptyList()
23 | }
24 | val description = description()
25 | val number = number()
26 | return Episode(
27 | description, number, animeName(), image(), video(), url,
28 | nextEpisodes
29 | )
30 | }
31 | }
32 |
33 | override fun isEpisode(url: String): Boolean =
34 | url.contains(URL_REGEX)
35 |
36 | private fun List?.toEpisode(animeName: String?): List {
37 | this?.let {
38 | return it.map {
39 | with(it) {
40 | Episode(description(), number(), animeName, image(), null, link())
41 | }
42 | }.toList()
43 | }
44 | return emptyList()
45 | }
46 |
47 | interface CurrentEpisode {
48 |
49 | @Selector("title")
50 | @TextCollector
51 | fun description(): String
52 |
53 | @Selector("title")
54 | @TextCollector
55 | @Regexp("^(?:.*)\\s+?(\\d++)")
56 | @ToInt
57 | fun number(): Int
58 |
59 | @Selector("[property=article:section]")
60 | @AttrCollector("content")
61 | fun animeName(): String?
62 |
63 | @Selector("[itemprop=thumbnailUrl]")
64 | @AttrCollector("content")
65 | fun image(): String?
66 |
67 | @Selector("video#video source")
68 | @AttrCollector("src")
69 | fun video(): String? = null
70 |
71 | @Selector(".episode a")
72 | @Nested
73 | fun nextEpisodes(): ArrayList?
74 | }
75 |
76 | interface NextEpisode {
77 |
78 | @Selector("#epnum")
79 | @TextCollector
80 | fun description(): String
81 |
82 | @Selector("#epnum")
83 | @TextCollector
84 | @Regex("(\\d+)")
85 | @ToInt
86 | fun number(): Int
87 |
88 | @Selector("#epimg img")
89 | @AttrCollector("src")
90 | fun image(): String?
91 |
92 | @Selector("a")
93 | @AttrCollector("href")
94 | fun link(): String
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_episode.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
21 |
22 |
33 |
34 |
48 |
49 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/explorer/src/test/kotlin/brunodles/urlfetcher/CacheFetcherTest.kt:
--------------------------------------------------------------------------------
1 | package brunodles.urlfetcher
2 |
3 | import com.greghaskins.spectrum.Spectrum
4 | import com.greghaskins.spectrum.Spectrum.describe
5 | import com.greghaskins.spectrum.Spectrum.it
6 | import org.junit.Assert.assertEquals
7 | import org.junit.runner.RunWith
8 |
9 | @RunWith(Spectrum::class)
10 | class CacheFetcherTest {
11 |
12 | companion object {
13 | val URLS = mapOf(
14 | "http://google.com.br" to "google.com.br/_index",
15 | "http://www.google.com.br" to "google.com.br/_index",
16 | "http://search.google.com.br" to "search.google.com.br/_index",
17 | "http://keep.google.com" to "keep.google.com/_index",
18 | "http://maps.google.com" to "maps.google.com/_index",
19 |
20 | "https://www.animekaionline.com/tsukipro-the-animation/episodio-1" to "animekaionline.com/tsukiprotheanimationepisodio1",
21 | "https://www.animesonlinebr.com.br/video/50034" to "animesonlinebr.com.br/video50034",
22 |
23 | "http://www.animesorion.video/71672" to "animesorion.video/71672",
24 | "http://www.animesorion.tv/71672" to "animesorion.tv/71672",
25 | "http://animesorion.tv" to "animesorion.tv/_index",
26 | "https://www.animesorion.site/71672" to "animesorion.site/71672",
27 | "http://animesorion.site" to "animesorion.site/_index",
28 |
29 | "https://animetubebrasil.com/1582/" to "animetubebrasil.com/1582",
30 | "http://animetubebrasil.com" to "animetubebrasil.com/_index",
31 |
32 | "https://www.anitubebr.com/vd/19249/" to "anitubebr.com/vd19249",
33 | "http://anitubebr.com" to "anitubebr.com/_index",
34 |
35 | "https://www.anitube.site/765/" to "anitube.site/765",
36 | "http://www.anitube.site" to "anitube.site/_index",
37 | "http://anitube.tv" to "anitube.tv/_index",
38 |
39 | "https://onepiece-ex.com.br/episodios/online/208/" to "onepieceex.com.br/episodiosonline208",
40 | "http://one-piece-x.com.br/episodios/online/207/" to "onepiecex.com.br/episodiosonline207",
41 | "http://onepiecex.com.br" to "onepiecex.com.br/_index",
42 | "http://tamanegi.onepiece-ex.com.br" to "tamanegi.onepieceex.com.br/_index",
43 | "http://onepiece-ex.com.br" to "onepieceex.com.br/_index",
44 |
45 | "http://tvcurse.com/?p=713" to "tvcurse.com/p713",
46 | "http://tvcurse.com" to "tvcurse.com/_index",
47 |
48 | "https://www.xvideos.com/video12026193/anita_troca_o_peluche_pelo_pau" to "xvideos.com/video12026193anita_troca_o_peluche_pelo_pau",
49 |
50 | "http://bagunca.subs.ow.no-ip.com.br" to "bagunca.subs.ow.noip.com.br/_index",
51 |
52 | "http://localhost:8888/redirect300" to "localhost8888/redirect300"
53 | )
54 | }
55 |
56 | init {
57 | describe("when parseToKey") {
58 | URLS.forEach { url, expectedKey ->
59 | describe("with $url") {
60 | it("should return \"$expectedKey\"") {
61 | assertEquals(expectedKey, CacheFetcher.urlToKey(url))
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/player/Player.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.player
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.util.Log
6 | import com.google.android.exoplayer2.C
7 | import com.google.android.exoplayer2.ExoPlayer
8 | import com.google.android.exoplayer2.ExoPlayerFactory
9 | import com.google.android.exoplayer2.SimpleExoPlayer
10 | import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
11 | import com.google.android.exoplayer2.source.ExtractorMediaSource
12 | import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection
13 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
14 | import com.google.android.exoplayer2.ui.SimpleExoPlayerView
15 | import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
16 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
17 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
18 | import com.google.android.exoplayer2.util.Util
19 |
20 | class Player(context: Context, playerView: SimpleExoPlayerView) {
21 |
22 | companion object {
23 | val TAG = "Player"
24 | val USER_AGENT = "AnimeWatcher"
25 | }
26 |
27 | private val exoPlayer: SimpleExoPlayer
28 | private val userAgent: String by lazy { Util.getUserAgent(context, USER_AGENT) }
29 | var onEndListener: (() -> Unit)? = null
30 |
31 | init {
32 | Log.d(TAG, "init: ")
33 | val bandwidthMeter = DefaultBandwidthMeter()
34 | val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
35 | val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
36 |
37 | exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector)
38 | playerView.player = exoPlayer
39 | exoPlayer.addListener(PlayerListener(this::onStateChanged))
40 | }
41 |
42 | private fun onStateChanged(state: Int) {
43 | if (state == ExoPlayer.STATE_ENDED)
44 | onEndListener?.invoke()
45 | }
46 |
47 | fun prepareVideo(url: String, playWhenReady: Boolean = false) {
48 | Log.d(TAG, "prepareVideo: $url")
49 | val bandwidthMeter = DefaultBandwidthMeter()
50 | val dataSourceFactory = DefaultHttpDataSourceFactory(userAgent, bandwidthMeter,
51 | DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
52 | DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true)
53 | val extractorsFactory = DefaultExtractorsFactory()
54 | val videoSource = ExtractorMediaSource(Uri.parse(url),
55 | dataSourceFactory, extractorsFactory, null, null)
56 | exoPlayer.prepare(videoSource, false, false)
57 | exoPlayer.playWhenReady = playWhenReady
58 | }
59 |
60 | fun getCurrentPosition() = exoPlayer.currentPosition
61 |
62 | fun stopAndRelease() {
63 | Log.d(TAG, "stopAndRelease: ")
64 | exoPlayer.stop()
65 | exoPlayer.release()
66 | }
67 |
68 | fun seekTo(position: Long) {
69 | Log.d(TAG, "seekTo: $position")
70 | exoPlayer.seekTo(position)
71 | exoPlayer.playWhenReady = true
72 | exoPlayer.playbackState
73 | }
74 | }
75 |
76 | fun Player?.getCurrentPosition() = this?.getCurrentPosition() ?: C.TIME_UNSET
--------------------------------------------------------------------------------
/app/src/main/manifest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
23 |
24 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
52 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ${intentfilter-data}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/nextepisodes/NextEpisodeFragment.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.nextepisodes
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.widget.GridLayoutManager
6 | import android.support.v7.widget.RecyclerView
7 | import android.util.Log
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import brunodles.animewatcher.R
12 | import brunodles.animewatcher.explorer.Episode
13 | import brunodles.animewatcher.home.HomeActivity
14 | import brunodles.animewatcher.persistence.Firebase
15 | import brunodles.animewatcher.player.PlayerActivity
16 | import com.google.firebase.auth.FirebaseAuth
17 | import com.google.firebase.auth.FirebaseUser
18 | import io.reactivex.android.schedulers.AndroidSchedulers
19 | import io.reactivex.disposables.CompositeDisposable
20 | import io.reactivex.rxkotlin.subscribeBy
21 | import io.reactivex.schedulers.Schedulers
22 | import java.util.concurrent.TimeUnit
23 |
24 | class NextEpisodeFragment : Fragment() {
25 | private lateinit var adapter: EpisodeAdapter
26 | private lateinit var recyclerView: RecyclerView
27 | private val disposable = CompositeDisposable()
28 |
29 | override fun onCreateView(
30 | inflater: LayoutInflater,
31 | container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View? {
34 | recyclerView = inflater.inflate(R.layout.fragment_episode, container, false) as RecyclerView
35 | adapter = EpisodeAdapter()
36 | recyclerView.adapter = adapter
37 | recyclerView.layoutManager = GridLayoutManager(
38 | this.context,
39 | resources.getInteger(R.integer.home_columns),
40 | GridLayoutManager.VERTICAL,
41 | true
42 | )
43 | return recyclerView
44 | }
45 |
46 | override fun onStart() {
47 | super.onStart()
48 | adapter.setEpisodeClickListener(::onItemClick)
49 | FirebaseAuth.getInstance().currentUser?.let { suggestNextEpisode(it) }
50 | }
51 |
52 | private fun onItemClick(episode: Episode) {
53 | context?.let {
54 | startActivity(PlayerActivity.newIntent(it, episode))
55 | }
56 | }
57 |
58 | override fun onStop() {
59 | super.onStop()
60 | adapter.setEpisodeClickListener(null)
61 | }
62 |
63 | private fun suggestNextEpisode(user: FirebaseUser) {
64 | adapter.clear()
65 | disposable.add(Firebase.nextEpisodes(user)
66 | .subscribeOn(Schedulers.io())
67 | .take(5, TimeUnit.SECONDS)
68 | .doOnNext {
69 | Log.d(
70 | HomeActivity.TAG,
71 | "suggestNextEpisode: ${it.animeName} - ${it.number} ${it.description}"
72 | )
73 | }
74 | .observeOn(AndroidSchedulers.mainThread())
75 | .subscribeBy(
76 | onNext = { adapter.add(it) },
77 | onError = { Log.e(HomeActivity.TAG, "suggestNextEpisode: ", it) },
78 | onComplete = {
79 | Log.d(
80 | HomeActivity.TAG,
81 | "suggestNextEpisode: nextAdapter.count = ${adapter.itemCount}"
82 | )
83 | }
84 | ))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/brunodles/animewatcher/player/EpisodeController.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.player
2 |
3 | //import brunodles.animewatcher.decoders.UrlChecker
4 | import android.content.Context
5 | import android.util.Log
6 | import brunodles.animewatcher.ImageLoader
7 | import brunodles.animewatcher.api.ApiFactory
8 | import brunodles.animewatcher.explorer.Episode
9 | import brunodles.animewatcher.parcelable.EpisodeParcel
10 | import brunodles.animewatcher.parcelable.EpisodeParceler
11 | import brunodles.animewatcher.persistence.Firebase
12 | import brunodles.rxfirebase.singleObservable
13 | import io.reactivex.Observable
14 | import io.reactivex.Single
15 | import io.reactivex.rxkotlin.subscribeBy
16 | import io.reactivex.schedulers.Schedulers
17 | import java.util.concurrent.TimeUnit
18 |
19 | class EpisodeController(val context: Context) {
20 |
21 | companion object {
22 | val TAG = "EpisodeController"
23 | }
24 |
25 | fun findVideoOn(url: String?): Single {
26 | if (url == null) return Single.error(NullPointerException("Empty Url"))
27 |
28 | return checkRemoveVideoInfo(url)
29 | .doOnSuccess(this::preFetchNextEpisodes)
30 | .doOnSuccess {
31 | Firebase.addToHistory(url)
32 | }
33 | }
34 |
35 | fun findVideoOn(episode: EpisodeParcel): Single {
36 | return if (episode.isVideoMissing())
37 | findVideoOn(episode.link)
38 | else
39 | Single.just(episode)
40 | .subscribeOn(Schedulers.io())
41 | .map(EpisodeParceler::fromParcel)
42 | .doOnSuccess { Firebase.addToHistory(episode.link) }
43 | }
44 |
45 | private fun checkRemoveVideoInfo(url: String): Single {
46 | return Firebase.videoRef(url).singleObservable(Episode::class.java)
47 | .map {
48 | if (it.isPlayable()) it
49 | else throw RuntimeException("Episode is not playable!")
50 | }
51 | .onErrorResumeNext(fetchVideo(url))
52 | }
53 |
54 | private fun fetchVideo(url: String): Single {
55 | Log.d(TAG, "fetchVideo: url: $url")
56 | return ApiFactory.api.decoder(url)
57 | .subscribeOn(Schedulers.io())
58 | .map(::getImageIfNeeded)
59 | .doOnSuccess { Firebase.addVideo(it) }
60 | .timeout(1, TimeUnit.MINUTES)
61 | }
62 |
63 | private fun getImageIfNeeded(episode: Episode): Episode {
64 | return if (episode.image.isNullOrBlank())
65 | episode.copy(image = ImageLoader.first("${episode.animeName} ${episode.number} ${episode.description}"))
66 | else
67 | episode
68 | }
69 |
70 | private fun preFetchNextEpisodes(episode: Episode) {
71 | if (!episode.containsNextEpisodes()) {
72 | Log.d(TAG, "preFetchNextEpisodes: nextEpisode is empty")
73 | return
74 | }
75 | Observable.fromIterable(episode.nextEpisodes)
76 | .subscribeOn(Schedulers.io())
77 | .doOnNext { Firebase.addVideo(it) }
78 | .flatMapSingle { fetchVideo(it.link) }
79 | .subscribeBy(onNext = {
80 | Firebase.addVideo(it)
81 | }, onError = {
82 | Log.e(TAG, "preFetchNextEpisodes: failed to fetch next episodes", it)
83 | })
84 | }
85 | }
--------------------------------------------------------------------------------
/explorer/src/main/kotlin/brunodles/animewatcher/decoders/AnimesOrionFactory.kt:
--------------------------------------------------------------------------------
1 | package brunodles.animewatcher.decoders
2 |
3 | import brunodles.animewatcher.AlchemistFactory
4 | import brunodles.animewatcher.ToInt
5 | import brunodles.animewatcher.explorer.Episode
6 | import brunodles.animewatcher.explorer.PageParser
7 | import brunodles.urlfetcher.UrlFetcher
8 | import brunodles.urlfetcher.href
9 | import com.brunodles.alchemist.collectors.AttrCollector
10 | import com.brunodles.alchemist.collectors.TextCollector
11 | import com.brunodles.alchemist.selector.Selector
12 | import org.jsoup.nodes.Document
13 | import java.util.regex.Pattern
14 |
15 | object AnimesOrionFactory : PageParser {
16 |
17 | @Suppress("RegExpRedundantEscape")
18 | private val URL_REGEX =
19 | Regex("^(?:https?\\:\\/\\/)?(?:www.)?animesorion.(\\w+)\\/(\\d+).*?\$")
20 |
21 | override fun isEpisode(url: String): Boolean =
22 | url.matches(URL_REGEX)
23 |
24 | override fun episode(url: String): Episode {
25 | val document = getUrl(url)
26 | val html = document.html()
27 | if (document.title().contains("todos", true))
28 | return parseAbout(document, url)
29 | return parsePlayer(html, url)
30 | }
31 |
32 | private fun parseAbout(html: Document, url: String): Episode {
33 | val links = html.select(".lcp_catlist a")
34 | links.sortBy { it.attr("href").extractWithRegex("(\\d+)").toInt() }
35 | val first = links.removeAt(0).attr("href")
36 | val episode = parsePlayer(getUrl(first).html(), url)
37 | return episode.copy(nextEpisodes = links.map {
38 | Episode(
39 | it.text(), it.text().extractWithRegex("^(?:.*)\\s+?(\\d++)").toInt(),
40 | episode.animeName, link = it.href()
41 | )
42 | }.toList())
43 | }
44 |
45 | private fun getUrl(url: String) = UrlFetcher.fetcher().get(url)
46 |
47 | fun parsePlayer(html: String, url: String): Episode {
48 | val currentEpisode = AlchemistFactory.alchemist.parseHtml(html, CurrentEpisode::class.java)
49 | with(currentEpisode) {
50 | val nextEpisodeLink = nextEpisode()
51 | val nextEpisodes = if (URL_REGEX.matches(nextEpisodeLink))
52 | listOf(Episode("Next", number() + 1, animeName(), link = nextEpisodeLink))
53 | else emptyList()
54 | return Episode(
55 | description(), number(), animeName(), null, video(), url,
56 | nextEpisodes
57 | )
58 | }
59 | }
60 |
61 | interface CurrentEpisode {
62 |
63 | @Selector("[itemprop=name]")
64 | @AttrCollector("content")
65 | fun description(): String
66 |
67 | @Selector("[itemprop=description]")
68 | @AttrCollector("content")
69 | @Regexp("^(?:.*)\\s+?(\\d++)")
70 | @ToInt
71 | fun number(): Int
72 |
73 | @Selector("[rel='category tag']")
74 | @TextCollector
75 | fun animeName(): String?
76 |
77 | @Selector("video source")
78 | @AttrCollector("src")
79 | fun video(): String?
80 |
81 | @Selector(".controlesVideo a:last-child")
82 | @AttrCollector("href")
83 | fun nextEpisode(): String
84 | }
85 | }
86 |
87 | private fun String.extractWithRegex(s: String): String {
88 | val matcher = Pattern.compile(s).matcher(this)
89 | if (matcher.find())
90 | return matcher.group(1)
91 | return ""
92 | }
93 |
--------------------------------------------------------------------------------