├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── discord.xml ├── gradle.xml └── kotlinScripting.xml ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── ani │ │ └── saikou │ │ ├── AnimeFragment.kt │ │ ├── AnimePageAdapter.kt │ │ ├── Functions.kt │ │ ├── GenreActivity.kt │ │ ├── HomeFragment.kt │ │ ├── LoginFragment.kt │ │ ├── MainActivity.kt │ │ ├── MangaFragment.kt │ │ ├── MangaPageAdapter.kt │ │ ├── Network.kt │ │ ├── NoInternet.kt │ │ ├── anilist │ │ ├── Anilist.kt │ │ ├── AnilistMutations.kt │ │ ├── AnilistQueries.kt │ │ ├── AnilistViewModel.kt │ │ ├── BannerImage.kt │ │ ├── Genre.kt │ │ ├── Login.kt │ │ ├── UrlMedia.kt │ │ └── api │ │ │ ├── Character.kt │ │ │ ├── Data.kt │ │ │ ├── FuzzyDate.kt │ │ │ ├── Media.kt │ │ │ ├── Page.kt │ │ │ ├── Recommendations.kt │ │ │ ├── Staff.kt │ │ │ ├── Studio.kt │ │ │ └── User.kt │ │ ├── anime │ │ ├── Anime.kt │ │ ├── AnimeSourceAdapter.kt │ │ ├── AnimeWatchAdapter.kt │ │ ├── AnimeWatchFragment.kt │ │ ├── Episode.kt │ │ ├── EpisodeAdapters.kt │ │ ├── ExoplayerView.kt │ │ ├── SelectorDialogFragment.kt │ │ ├── SubtitleDialogFragment.kt │ │ └── VideoCache.kt │ │ ├── manga │ │ ├── Manga.kt │ │ ├── MangaChapter.kt │ │ ├── MangaChapterAdapter.kt │ │ ├── MangaReadAdapter.kt │ │ ├── MangaReadFragment.kt │ │ ├── MangaSourceAdapter.kt │ │ └── mangareader │ │ │ ├── BaseImageAdapter.kt │ │ │ ├── DualPageAdapter.kt │ │ │ ├── ImageAdapter.kt │ │ │ ├── MangaReaderActivity.kt │ │ │ ├── PreloadLinearLayoutManager.kt │ │ │ ├── ReaderSettingsDialogFragment.kt │ │ │ └── Swipy.kt │ │ ├── media │ │ ├── CalendarActivity.kt │ │ ├── Character.kt │ │ ├── CharacterAdapter.kt │ │ ├── CharacterDetailsActivity.kt │ │ ├── CharacterDetailsAdapter.kt │ │ ├── GenreAdapter.kt │ │ ├── Media.kt │ │ ├── MediaAdaptor.kt │ │ ├── MediaDetailsActivity.kt │ │ ├── MediaDetailsViewModel.kt │ │ ├── MediaInfoFragment.kt │ │ ├── MediaListDialogFragment.kt │ │ ├── MediaListDialogSmallFragment.kt │ │ ├── OtherDetailsViewModel.kt │ │ ├── ProgressAdapter.kt │ │ ├── SearchActivity.kt │ │ ├── SearchAdapter.kt │ │ ├── SearchFilterBottomDialog.kt │ │ ├── Selected.kt │ │ ├── Source.kt │ │ ├── SourceAdapter.kt │ │ ├── SourceSearchDialogFragment.kt │ │ ├── Studio.kt │ │ ├── StudioActivity.kt │ │ └── TitleAdapter.kt │ │ ├── others │ │ ├── .gitignore │ │ ├── AniSkip.kt │ │ ├── AnimeFillerList.kt │ │ ├── AppUpdater.kt │ │ ├── CustomBottomDialog.kt │ │ ├── DisabledReports.kt │ │ ├── GlideApp.kt │ │ ├── Idiosyncrasy.kt │ │ ├── ImageViewDialog.kt │ │ ├── Kitsu.kt │ │ ├── Mal.kt │ │ ├── MalSyncBackup.kt │ │ ├── OutlineTextView.kt │ │ ├── ResettableTimer.kt │ │ ├── SpoilerPlugin.kt │ │ └── Xpandable.kt │ │ ├── parsers │ │ ├── AnimeParser.kt │ │ ├── AnimeSources.kt │ │ ├── BaseParser.kt │ │ ├── BaseSources.kt │ │ ├── MangaParser.kt │ │ ├── MangaSources.kt │ │ ├── VideoExtractor.kt │ │ ├── anime │ │ │ ├── AllAnime.kt │ │ │ ├── AnimePahe.kt │ │ │ ├── Animefenix.kt │ │ │ ├── Animelatinohd.kt │ │ │ ├── ConsumeBili.kt │ │ │ ├── Gogo.kt │ │ │ ├── Haho.kt │ │ │ ├── Henaojara.kt │ │ │ ├── HentaiFF.kt │ │ │ ├── HentaiMama.kt │ │ │ ├── HentaiStream.kt │ │ │ ├── Jkanime.kt │ │ │ ├── Marin.kt │ │ │ ├── Monoschinos.kt │ │ │ ├── NineAnime.kt │ │ │ ├── TioAnime.kt │ │ │ ├── Zoro.kt │ │ │ └── extractors │ │ │ │ ├── FPlayer.kt │ │ │ │ ├── GogoCDN.kt │ │ │ │ ├── OK.kt │ │ │ │ ├── PStream.kt │ │ │ │ ├── RapidCloud.kt │ │ │ │ ├── StreamSB.kt │ │ │ │ ├── StreamTape.kt │ │ │ │ ├── VideoVard.kt │ │ │ │ └── VizCloud.kt │ │ └── manga │ │ │ ├── AllAnime.kt │ │ │ ├── ComickFun.kt │ │ │ ├── Manga4Life.kt │ │ │ ├── MangaBuddy.kt │ │ │ ├── MangaDex.kt │ │ │ ├── MangaHub.kt │ │ │ ├── MangaKakalot.kt │ │ │ ├── MangaKatana.kt │ │ │ ├── MangaPill.kt │ │ │ ├── MangaRead.kt │ │ │ ├── MangaReaderTo.kt │ │ │ ├── Manhwa18.kt │ │ │ ├── NHentai.kt │ │ │ ├── NineHentai.kt │ │ │ └── Toonily.kt │ │ ├── settings │ │ ├── CurrentReaderSettings.kt │ │ ├── Developer.kt │ │ ├── DevelopersAdapter.kt │ │ ├── DevelopersDialogFragment.kt │ │ ├── FAQActivity.kt │ │ ├── FAQAdapter.kt │ │ ├── ForksDialogFragment.kt │ │ ├── PlayerSettings.kt │ │ ├── PlayerSettingsActivity.kt │ │ ├── ReaderSettings.kt │ │ ├── ReaderSettingsActivity.kt │ │ ├── SettingsActivity.kt │ │ ├── SettingsDialogFragment.kt │ │ ├── TVConnectionActivity.kt │ │ ├── UserInterfaceSettings.kt │ │ └── UserInterfaceSettingsActivity.kt │ │ ├── tv │ │ ├── TVAnimeDetailFragment.kt │ │ ├── TVAnimeDetailInfoFragment.kt │ │ ├── TVAnimeFragment.kt │ │ ├── TVDetailActivity.kt │ │ ├── TVGridSelectorFragment.kt │ │ ├── TVMainActivity.kt │ │ ├── TVMediaPlayer.kt │ │ ├── TVSearchFragment.kt │ │ ├── TVSelectorFragment.kt │ │ ├── TVSourceSelectorFragment.kt │ │ ├── components │ │ │ ├── ButtonListRow.kt │ │ │ ├── CustomListRowPresenter.kt │ │ │ ├── DetailsOverviewPresenter.kt │ │ │ ├── HeaderOnlyRow.kt │ │ │ ├── NonOverlappingFrameLayout.kt │ │ │ └── SearchFragment.kt │ │ ├── login │ │ │ ├── NetworkTVConnection.kt │ │ │ ├── TVLoginFragment.kt │ │ │ └── TVNetworkLoginFragment.kt │ │ ├── presenters │ │ │ ├── AnimePresenter.kt │ │ │ ├── ButtonListRowPresenter.kt │ │ │ ├── CharacterPresenter.kt │ │ │ ├── DetailActionsPresenter.kt │ │ │ ├── DetailInfoActionPresenter.kt │ │ │ ├── DetailsInfoPresenter.kt │ │ │ ├── DetailsWatchPresenter.kt │ │ │ ├── EpisodePresenter.kt │ │ │ ├── GenresPresenter.kt │ │ │ ├── HeaderRowPresenter.kt │ │ │ ├── MainHeaderPresenter.kt │ │ │ └── TagsPresenter.kt │ │ └── utils │ │ │ └── VideoPlayerGlue.kt │ │ └── user │ │ ├── ListActivity.kt │ │ ├── ListFragment.kt │ │ ├── ListViewModel.kt │ │ └── ListViewPagerAdapter.kt │ └── res │ ├── anim │ └── over_shoot.xml │ ├── color │ ├── button_switch_track.xml │ ├── tab_layout_icon.xml │ └── tab_layout_text.xml │ ├── drawable-xhdpi │ ├── banner.png │ ├── banner2.png │ └── saikouflush.png │ ├── drawable │ ├── anim_pause_to_play.xml │ ├── anim_play_to_pause.xml │ ├── anim_rewind.xml │ ├── anim_skip.xml │ ├── anim_splash.xml │ ├── bottom_nav.xml │ ├── control_background_40dp.xml │ ├── ic_anilist.xml │ ├── ic_baseline_screen_lock_portrait_24.xml │ ├── ic_bmc_button.xml │ ├── ic_discord.xml │ ├── ic_github.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_logo.xml │ ├── ic_page_numbering.xml │ ├── ic_round_accessible_forward_24.xml │ ├── ic_round_add_circle_24.xml │ ├── ic_round_add_circle_outline_24.xml │ ├── ic_round_alpha_t_box_24.xml │ ├── ic_round_amp_stories_24.xml │ ├── ic_round_animation_24.xml │ ├── ic_round_arrow_back_ios_new_24.xml │ ├── ic_round_arrow_drop_down_24.xml │ ├── ic_round_art_track_24.xml │ ├── ic_round_audiotrack_24.xml │ ├── ic_round_auto_awesome_24.xml │ ├── ic_round_brightness_4_24.xml │ ├── ic_round_brightness_auto_24.xml │ ├── ic_round_brightness_high_24.xml │ ├── ic_round_brightness_medium_24.xml │ ├── ic_round_calendar_today_24.xml │ ├── ic_round_cast_24.xml │ ├── ic_round_close_24.xml │ ├── ic_round_collections_bookmark_24.xml │ ├── ic_round_color_24.xml │ ├── ic_round_date_range_24.xml │ ├── ic_round_dns_24.xml │ ├── ic_round_download_24.xml │ ├── ic_round_fast_forward_24.xml │ ├── ic_round_fast_rewind_24.xml │ ├── ic_round_favorite_24.xml │ ├── ic_round_favorite_border_24.xml │ ├── ic_round_filter_alt_24.xml │ ├── ic_round_font_size_24.xml │ ├── ic_round_format_text_24.xml │ ├── ic_round_fullscreen_24.xml │ ├── ic_round_grid_view_24.xml │ ├── ic_round_heart_broken_24.xml │ ├── ic_round_help_24.xml │ ├── ic_round_high_quality_24.xml │ ├── ic_round_home_24.xml │ ├── ic_round_import_contacts_24.xml │ ├── ic_round_info_24.xml │ ├── ic_round_lock_24.xml │ ├── ic_round_lock_open_24.xml │ ├── ic_round_menu_book_24.xml │ ├── ic_round_movie_filter_24.xml │ ├── ic_round_new_releases_24.xml │ ├── ic_round_notifications_active_24.xml │ ├── ic_round_notifications_none_24.xml │ ├── ic_round_pause_24.xml │ ├── ic_round_person_24.xml │ ├── ic_round_photo_size_select_actual_24.xml │ ├── ic_round_picture_in_picture_alt_24.xml │ ├── ic_round_play_arrow_24.xml │ ├── ic_round_play_circle_24.xml │ ├── ic_round_play_disabled_24.xml │ ├── ic_round_playlist_add_24.xml │ ├── ic_round_playlist_play_24.xml │ ├── ic_round_reader_settings.xml │ ├── ic_round_refresh_24.xml │ ├── ic_round_remove_red_eye_24.xml │ ├── ic_round_restaurant_24.xml │ ├── ic_round_screen_rotation_24.xml │ ├── ic_round_screen_rotation_alt_24.xml │ ├── ic_round_sd_card_24.xml │ ├── ic_round_search_24.xml │ ├── ic_round_settings_24.xml │ ├── ic_round_share_24.xml │ ├── ic_round_skip_next_24.xml │ ├── ic_round_skip_previous_24.xml │ ├── ic_round_slow_motion_video_24.xml │ ├── ic_round_smart_button_24.xml │ ├── ic_round_sort_24.xml │ ├── ic_round_source_24.xml │ ├── ic_round_space_bar_24.xml │ ├── ic_round_star_24.xml │ ├── ic_round_straighten_24.xml │ ├── ic_round_subtitles_24.xml │ ├── ic_round_swipe_down_alt_24.xml │ ├── ic_round_swipe_up_alt_24.xml │ ├── ic_round_swipe_vertical_24.xml │ ├── ic_round_sync_24.xml │ ├── ic_round_touch_app_24.xml │ ├── ic_round_translate_variant_24.xml │ ├── ic_round_video_library_24.xml │ ├── ic_round_video_settings_24.xml │ ├── ic_round_view_array_24.xml │ ├── ic_round_view_column_24.xml │ ├── ic_round_view_comfy_24.xml │ ├── ic_round_view_list_24.xml │ ├── ic_round_volume_up_24.xml │ ├── ic_skip.xml │ ├── ic_telegram.xml │ ├── ic_tvlogin.xml │ ├── ic_upi_icon.xml │ ├── item_ongoing.xml │ ├── item_score.xml │ ├── item_type.xml │ ├── item_user_score.xml │ ├── linear_gradient_bg.xml │ ├── linear_gradient_black.xml │ ├── linear_gradient_black_horizontal.xml │ ├── linear_gradient_nav.xml │ ├── round_corner.xml │ ├── rounded_top_nav.xml │ ├── shape_corner_16dp.xml │ ├── spinner_icon.xml │ ├── spinner_icon_manga.xml │ └── ui_bg.xml │ ├── font │ ├── poppins.ttf │ ├── poppins_bold.ttf │ ├── poppins_family.xml │ ├── poppins_semi_bold.ttf │ └── poppins_thin.ttf │ ├── layout-land │ └── activity_media.xml │ ├── layout │ ├── activity_character.xml │ ├── activity_exoplayer.xml │ ├── activity_faq.xml │ ├── activity_genre.xml │ ├── activity_list.xml │ ├── activity_main.xml │ ├── activity_manga_reader.xml │ ├── activity_media.xml │ ├── activity_no_internet.xml │ ├── activity_player_settings.xml │ ├── activity_reader_settings.xml │ ├── activity_search.xml │ ├── activity_settings.xml │ ├── activity_studio.xml │ ├── activity_user_interface_settings.xml │ ├── bottom_sheet_current_reader_settings.xml │ ├── bottom_sheet_custom.xml │ ├── bottom_sheet_developers.xml │ ├── bottom_sheet_image.xml │ ├── bottom_sheet_media_list.xml │ ├── bottom_sheet_media_list_small.xml │ ├── bottom_sheet_search_filter.xml │ ├── bottom_sheet_selector.xml │ ├── bottom_sheet_settings.xml │ ├── bottom_sheet_source_search.xml │ ├── bottom_sheet_subtitles.xml │ ├── exo_styled_player_control_view.xml │ ├── exo_styled_player_view.xml │ ├── fragment_anime.xml │ ├── fragment_anime_watch.xml │ ├── fragment_home.xml │ ├── fragment_list.xml │ ├── fragment_login.xml │ ├── fragment_manga.xml │ ├── fragment_media_info.xml │ ├── fragment_tv_connection.xml │ ├── item_anime_page.xml │ ├── item_anime_watch.xml │ ├── item_chapter_list.xml │ ├── item_character.xml │ ├── item_character_details.xml │ ├── item_chip.xml │ ├── item_count_down.xml │ ├── item_developer.xml │ ├── item_dropdown.xml │ ├── item_dual_page.xml │ ├── item_episode_compact.xml │ ├── item_episode_grid.xml │ ├── item_episode_list.xml │ ├── item_episodes_recyclerview.xml │ ├── item_genre.xml │ ├── item_image.xml │ ├── item_manga_page.xml │ ├── item_media_compact.xml │ ├── item_media_large.xml │ ├── item_media_page.xml │ ├── item_media_page_small.xml │ ├── item_progressbar.xml │ ├── item_quels.xml │ ├── item_question.xml │ ├── item_recyclerview.xml │ ├── item_search_header.xml │ ├── item_seekbar_dialog.xml │ ├── item_stream.xml │ ├── item_subtitle_text.xml │ ├── item_title.xml │ ├── item_title_chipgroup.xml │ ├── item_title_recycler.xml │ ├── item_title_text.xml │ ├── item_title_trailer.xml │ ├── item_url.xml │ ├── splash_screen.xml │ ├── tv_activity_main.xml │ ├── tv_anime_card.xml │ ├── tv_anime_detail.xml │ ├── tv_anime_detail_info.xml │ ├── tv_anime_detail_info_overview.xml │ ├── tv_button_row_header.xml │ ├── tv_detail_action.xml │ ├── tv_detail_activity.xml │ ├── tv_detail_info_title.xml │ ├── tv_episode_card.xml │ ├── tv_genre_card.xml │ ├── tv_item_character.xml │ ├── tv_item_chip.xml │ ├── tv_item_source.xml │ ├── tv_item_url.xml │ ├── tv_network_login_fragment.xml │ ├── tv_progress_row_header.xml │ ├── tv_row_header.xml │ └── tv_search_fragment.xml │ ├── menu │ ├── anime_menu_detail.xml │ ├── bottom_navbar_menu.xml │ ├── list_sort_menu.xml │ ├── manga_menu_detail.xml │ └── menu_media.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-es │ └── strings.xml │ ├── values-night │ ├── colors.xml │ └── themes.xml │ ├── values │ ├── attr.xml │ ├── colors.xml │ ├── dimens.xml │ ├── font_certs.xml │ ├── preloaded_fonts.xml │ ├── strings.xml │ ├── style.xml │ └── themes.xml │ └── xml │ └── provider_paths.xml ├── beta.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── stable.md └── stable.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | build 5 | local.properties 6 | .DS_Store 7 | /captures 8 | .externalNativeBuild 9 | .cxx -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /debug/output-metadata.json 4 | /release -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "71148296899", 4 | "project_id": "saikoutv-f0a81", 5 | "storage_bucket": "saikoutv-f0a81.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:71148296899:android:10c3ede77ee5e43aafa076", 11 | "android_client_info": { 12 | "package_name": "ani.saikou" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "71148296899-e8kl3j2ieb8b3f2fl95e44bl2ae3srdl.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "ani.saikou", 21 | "certificate_hash": "cd5db26708a5ee22d4e740010f77a70dcb0a8519" 22 | } 23 | }, 24 | { 25 | "client_id": "71148296899-fk1jokciegdamvjbv97e5tbj5fpe8fal.apps.googleusercontent.com", 26 | "client_type": 1, 27 | "android_info": { 28 | "package_name": "ani.saikou", 29 | "certificate_hash": "71bc71da9b878fb691da817070c610784b863409" 30 | } 31 | }, 32 | { 33 | "client_id": "71148296899-3tk4josah9mb0h32u6i276lka6hsbjio.apps.googleusercontent.com", 34 | "client_type": 3 35 | } 36 | ], 37 | "api_key": [ 38 | { 39 | "current_key": "AIzaSyBN7D-3RArG5bQ-HEzrEP_JOcHqFs0sk9g" 40 | } 41 | ], 42 | "services": { 43 | "appinvite_service": { 44 | "other_platform_oauth_client": [ 45 | { 46 | "client_id": "71148296899-3tk4josah9mb0h32u6i276lka6hsbjio.apps.googleusercontent.com", 47 | "client_type": 3 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | ], 54 | "configuration_version": "1" 55 | } -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Saikou β 4 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanoc6/SaikouTV/6a96beb7a4443353ab5cc0b1b4ac5c0de5e74b9e/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import ani.saikou.anilist.Anilist 9 | import ani.saikou.databinding.FragmentLoginBinding 10 | 11 | class LoginFragment : Fragment() { 12 | 13 | private var _binding: FragmentLoginBinding? = null 14 | private val binding get() = _binding!! 15 | 16 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 17 | _binding = FragmentLoginBinding.inflate(layoutInflater, container, false) 18 | return binding.root 19 | } 20 | 21 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 22 | binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) } 23 | binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) } 24 | binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) } 25 | binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/NoInternet.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou 2 | 3 | import android.os.Bundle 4 | import android.view.ViewGroup 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.view.updateLayoutParams 7 | import ani.saikou.databinding.ActivityNoInternetBinding 8 | 9 | class NoInternet : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | val binding = ActivityNoInternetBinding.inflate(layoutInflater) 14 | setContentView(binding.root) 15 | 16 | binding.refreshContainer.updateLayoutParams { 17 | topMargin = statusBarHeight 18 | bottomMargin = navBarHeight 19 | } 20 | binding.refreshButton.setOnClickListener { 21 | if (isOnline(this)) { 22 | startMainActivity(this) 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/BannerImage.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist 2 | 3 | import java.io.Serializable 4 | 5 | data class BannerImage( 6 | val url: String?, 7 | var time: Long, 8 | ) : Serializable { 9 | fun checkTime(): Boolean { 10 | return (System.currentTimeMillis() - time) >= (1000 * 60 * 60 * 6) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/Genre.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist 2 | 3 | import java.io.Serializable 4 | 5 | data class Genre( 6 | val name: String, 7 | var id: Int, 8 | var thumbnail: String, 9 | var time: Long, 10 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/Login.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import ani.saikou.logger 8 | import ani.saikou.logError 9 | import ani.saikou.startMainActivity 10 | 11 | class Login : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val data: Uri? = intent?.data 15 | logger(data.toString()) 16 | try { 17 | Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value 18 | val filename = "anilistToken" 19 | this.openFileOutput(filename, Context.MODE_PRIVATE).use { 20 | it.write(Anilist.token!!.toByteArray()) 21 | } 22 | } catch (e: Exception) { 23 | logError(e) 24 | } 25 | startMainActivity(this) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/UrlMedia.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist 2 | 3 | import android.net.Uri 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import ani.saikou.loadIsMAL 7 | import ani.saikou.loadMedia 8 | import ani.saikou.logError 9 | import ani.saikou.startMainActivity 10 | 11 | class UrlMedia : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val data: Uri? = intent?.data 15 | if (data?.host != "anilist.co") loadIsMAL = true 16 | try { 17 | if (data?.pathSegments?.get(1) != null) loadMedia = data.pathSegments?.get(1)?.toIntOrNull() 18 | } catch (e: Exception) { 19 | logError(e) 20 | } 21 | startMainActivity(this) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/api/FuzzyDate.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import java.io.Serializable 5 | import java.text.DateFormatSymbols 6 | import java.util.* 7 | 8 | @kotlinx.serialization.Serializable 9 | data class FuzzyDate( 10 | @SerialName("year") val year: Int? = null, 11 | @SerialName("month") val month: Int? = null, 12 | @SerialName("day") val day: Int? = null, 13 | ) : Serializable { 14 | override fun toString(): String { 15 | if(day==null && year==null && month==null) 16 | return "??" 17 | val a = if (month != null) DateFormatSymbols().months[month - 1] else "" 18 | return (if (day != null) "$day " else "") + a + (if (year != null) ", $year" else "") 19 | } 20 | 21 | fun getToday(): FuzzyDate { 22 | val cal = Calendar.getInstance() 23 | return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)) 24 | } 25 | 26 | fun toVariableString(): String { 27 | return ("{" 28 | + (if (year != null) "year:$year" else "") 29 | + (if (month != null) ",month:$month" else "") 30 | + (if (day != null) ",day:$day" else "") 31 | + "}") 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/api/Recommendations.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | @Serializable 6 | data class Recommendation( 7 | // The id of the recommendation 8 | @SerialName("id") var id: Int?, 9 | 10 | // Users rating of the recommendation 11 | @SerialName("rating") var rating: Int?, 12 | 13 | // The rating of the recommendation by currently authenticated user 14 | // @SerialName("userRating") var userRating: RecommendationRating?, 15 | 16 | // The media the recommendation is from 17 | @SerialName("media") var media: Media?, 18 | 19 | // The recommended media 20 | @SerialName("mediaRecommendation") var mediaRecommendation: Media?, 21 | 22 | // The user that first created the recommendation 23 | @SerialName("user") var user: User?, 24 | ) 25 | @Serializable 26 | data class RecommendationConnection( 27 | //@SerialName("edges") var edges: List?, 28 | 29 | @SerialName("nodes") var nodes: List?, 30 | 31 | // The pagination information 32 | //@SerialName("pageInfo") var pageInfo: PageInfo?, 33 | 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anilist/api/Studio.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anilist.api 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Studio( 8 | // The id of the studio 9 | @SerialName("id") var id: Int, 10 | 11 | // The name of the studio 12 | // Originally non-nullable, needs to be nullable due to it not being always queried 13 | @SerialName("name") var name: String?, 14 | 15 | // If the studio is an animation studio or a different kind of company 16 | @SerialName("isAnimationStudio") var isAnimationStudio: Boolean?, 17 | 18 | // The media the studio has worked on 19 | @SerialName("media") var media: MediaConnection?, 20 | 21 | // The url for the studio page on the AniList website 22 | @SerialName("siteUrl") var siteUrl: String?, 23 | 24 | // If the studio is marked as favourite by the currently authenticated user 25 | @SerialName("isFavourite") var isFavourite: Boolean?, 26 | 27 | // The amount of user's who have favourited the studio 28 | @SerialName("favourites") var favourites: Int?, 29 | ) 30 | 31 | @Serializable 32 | data class StudioConnection( 33 | //@SerialName("edges") var edges: List?, 34 | 35 | @SerialName("nodes") var nodes: List?, 36 | 37 | // The pagination information 38 | //@SerialName("pageInfo") var pageInfo: PageInfo?, 39 | ) -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anime/Anime.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anime 2 | 3 | import ani.saikou.media.Studio 4 | import java.io.Serializable 5 | 6 | data class Anime( 7 | var totalEpisodes: Int? = null, 8 | 9 | var episodeDuration: Int? = null, 10 | var season: String? = null, 11 | var seasonYear: Int? = null, 12 | 13 | var op: ArrayList = arrayListOf(), 14 | var ed: ArrayList = arrayListOf(), 15 | 16 | var mainStudio: Studio? = null, 17 | 18 | var youtube: String? = null, 19 | var nextAiringEpisode: Int? = null, 20 | var nextAiringEpisodeTime: Long? = null, 21 | 22 | var selectedEpisode: String? = null, 23 | var episodes: MutableMap? = null, 24 | var slug: String? = null, 25 | var kitsuEpisodes: Map? = null, 26 | var fillerEpisodes: Map? = null, 27 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anime/AnimeSourceAdapter.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anime 2 | 3 | import ani.saikou.media.MediaDetailsViewModel 4 | import ani.saikou.media.SourceAdapter 5 | import ani.saikou.media.SourceSearchDialogFragment 6 | import ani.saikou.parsers.ShowResponse 7 | import kotlinx.coroutines.CoroutineScope 8 | 9 | class AnimeSourceAdapter( 10 | sources: List, 11 | val model: MediaDetailsViewModel, 12 | val i: Int, 13 | val id: Int, 14 | fragment: SourceSearchDialogFragment, 15 | scope: CoroutineScope 16 | ) : SourceAdapter(sources, fragment, scope) { 17 | 18 | override suspend fun onItemClick(source: ShowResponse) { 19 | model.overrideEpisodes(i, source, id) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anime/Episode.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anime 2 | 3 | import ani.saikou.FileUrl 4 | import ani.saikou.parsers.VideoExtractor 5 | import java.io.Serializable 6 | 7 | data class Episode( 8 | val number: String, 9 | var link: String? = null, 10 | var title: String? = null, 11 | var desc: String? = null, 12 | var thumb: FileUrl? = null, 13 | var filler: Boolean = false, 14 | var selectedExtractor: String? = null, 15 | var selectedVideo: Int = 0, 16 | var selectedSubtitle: Int? = -1, 17 | var extractors: MutableList?=null, 18 | @Transient var extractorCallback: ((VideoExtractor) -> Unit)?=null, 19 | var allStreams: Boolean = false, 20 | var watched: Long? = null, 21 | var maxLength: Long? = null, 22 | val extra: Any?=null, 23 | 24 | ) : Serializable 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/anime/VideoCache.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.anime 2 | 3 | import android.content.Context 4 | import com.google.android.exoplayer2.database.StandaloneDatabaseProvider 5 | import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor 6 | import com.google.android.exoplayer2.upstream.cache.SimpleCache 7 | import java.io.File 8 | 9 | object VideoCache { 10 | private var simpleCache: SimpleCache? = null 11 | fun getInstance(context: Context): SimpleCache { 12 | val databaseProvider = StandaloneDatabaseProvider(context) 13 | if (simpleCache == null) 14 | simpleCache = SimpleCache( 15 | File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file 16 | LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L), 17 | databaseProvider 18 | ) 19 | return simpleCache as SimpleCache 20 | } 21 | 22 | fun release() { 23 | simpleCache?.release() 24 | simpleCache = null 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/manga/Manga.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.manga 2 | 3 | import java.io.Serializable 4 | 5 | data class Manga( 6 | var totalChapters: Int? = null, 7 | var selectedChapter: String? = null, 8 | var chapters: MutableMap? = null, 9 | var slug: String? = null, 10 | var author: String?=null, 11 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/manga/MangaChapter.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.manga 2 | 3 | import ani.saikou.parsers.MangaChapter 4 | import ani.saikou.parsers.MangaImage 5 | import java.io.Serializable 6 | import kotlin.math.ceil 7 | 8 | data class MangaChapter( 9 | val number: String, 10 | var link: String, 11 | var title: String? = null, 12 | var description: String? = null, 13 | var images: List? = null 14 | ) : Serializable { 15 | constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description) 16 | 17 | private var dualPage: List>? = null 18 | fun dualPages(): List> { 19 | dualPage = dualPage ?: (0..ceil((images!!.size.toFloat() - 1f) / 2).toInt()).map { 20 | val i = it * 2 21 | (images?.getOrNull(i) to images?.getOrNull(i + 1)) 22 | } 23 | return dualPage!! 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/manga/MangaSourceAdapter.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.manga 2 | 3 | import ani.saikou.media.MediaDetailsViewModel 4 | import ani.saikou.media.SourceAdapter 5 | import ani.saikou.media.SourceSearchDialogFragment 6 | import ani.saikou.parsers.ShowResponse 7 | import kotlinx.coroutines.CoroutineScope 8 | 9 | class MangaSourceAdapter( 10 | sources: List, 11 | val model: MediaDetailsViewModel, 12 | val i: Int, 13 | val id: Int, 14 | fragment: SourceSearchDialogFragment, 15 | scope: CoroutineScope 16 | ) : SourceAdapter(sources, fragment, scope) { 17 | override suspend fun onItemClick(source: ShowResponse) { 18 | model.overrideMangaChapters(i, source, id) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/Character.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import ani.saikou.anilist.api.FuzzyDate 4 | import java.io.Serializable 5 | 6 | data class Character( 7 | val id: Int, 8 | val name: String?, 9 | val image: String?, 10 | val banner: String?, 11 | val role: String, 12 | 13 | var description: String? = null, 14 | var age: String? = null, 15 | var gender: String? = null, 16 | var dateOfBirth: FuzzyDate? = null, 17 | var roles: ArrayList? = null 18 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/CharacterDetailsAdapter.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import ani.saikou.databinding.ItemCharacterDetailsBinding 9 | import ani.saikou.others.SpoilerPlugin 10 | import io.noties.markwon.Markwon 11 | import io.noties.markwon.SoftBreakAddsNewLinePlugin 12 | 13 | class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) : 14 | RecyclerView.Adapter() { 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder { 16 | val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false) 17 | return GenreViewHolder(binding) 18 | } 19 | 20 | @SuppressLint("SetTextI18n") 21 | override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { 22 | val binding = holder.binding 23 | val desc = 24 | (if (character.age != "null") "__Age:__ " + character.age else "") + 25 | (if (character.dateOfBirth.toString() != "") "\n__Birthday:__ " + character.dateOfBirth.toString() else "") + 26 | (if (character.gender != "null") "\n__Gender:__ " + character.gender else "") + "\n" + character.description 27 | 28 | binding.characterDesc.isTextSelectable 29 | val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build() 30 | markWon.setMarkdown(binding.characterDesc, desc) 31 | 32 | } 33 | 34 | override fun getItemCount(): Int = 1 35 | inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/Selected.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import java.io.Serializable 4 | 5 | data class Selected( 6 | var window: Int = 0, 7 | var recyclerStyle: Int? = null, 8 | var recyclerReversed: Boolean = false, 9 | var chip: Int = 0, 10 | var source: Int = 0, 11 | var preferDub: Boolean = false, 12 | var server: String? = null, 13 | var video: Int = 0, 14 | ) : Serializable 15 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/Source.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import java.io.Serializable 4 | 5 | data class Source( 6 | val link: String, 7 | val name: String, 8 | val cover: String, 9 | val headers: MutableMap? = null 10 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/Studio.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import java.io.Serializable 4 | 5 | data class Studio( 6 | val id: String, 7 | val name: String, 8 | var yearMedia: MutableMap>? = null 9 | ) : Serializable 10 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/media/TitleAdapter.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.media 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import ani.saikou.databinding.ItemTitleBinding 7 | 8 | class TitleAdapter(private val text: String) : RecyclerView.Adapter() { 9 | inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root) 10 | 11 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder { 12 | val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false) 13 | return TitleViewHolder(binding) 14 | } 15 | 16 | override fun onBindViewHolder(holder: TitleViewHolder, position: Int) { 17 | holder.binding.itemTitle.text = text 18 | } 19 | 20 | override fun getItemCount(): Int = 1 21 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/.gitignore: -------------------------------------------------------------------------------- 1 | DisabledReports.kt -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/AnimeFillerList.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.others 2 | 3 | import ani.saikou.anime.Episode 4 | import ani.saikou.client 5 | import ani.saikou.tryWithSuspend 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | object AnimeFillerList { 10 | suspend fun getFillers(malId: Int): Map? { 11 | return tryWithSuspend { 12 | val json = client.get("https://raw.githubusercontent.com/saikou-app/mal-id-filler-list/main/fillers/$malId.json") 13 | return@tryWithSuspend if (json.text != "404: Not Found") json.parsed().episodes?.associate { 14 | val num = it.number.toString() 15 | num to Episode( 16 | num, 17 | it.title, 18 | filler = it.fillerBool == true 19 | ) 20 | } else null 21 | } 22 | } 23 | 24 | @Serializable 25 | data class AnimeFillerListValue ( 26 | @SerialName("MAL-id") 27 | val malID: Int? = null, 28 | 29 | @SerialName("Anilist-id") 30 | val anilistID: Int? = null, 31 | 32 | val episodes: List? = null 33 | ) 34 | 35 | @Serializable 36 | data class AFLEpisode ( 37 | val number: Int? = null, 38 | val title: String? = null, 39 | val desc: String? = null, 40 | val filler: String? = null, 41 | @SerialName("filler-bool") val fillerBool : Boolean?=null, 42 | val airDate: String? = null 43 | ) 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/DisabledReports.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.others 2 | 3 | const val DisabledReports=true 4 | //Setting this to false, will allow sending crash reports to Saikou's Firebase Crashlytics -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/GlideApp.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.others 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import ani.saikou.okHttpClient 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.GlideBuilder 8 | import com.bumptech.glide.Registry 9 | import com.bumptech.glide.annotation.GlideModule 10 | import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader 11 | import com.bumptech.glide.load.engine.DiskCacheStrategy 12 | import com.bumptech.glide.load.model.GlideUrl 13 | import com.bumptech.glide.module.AppGlideModule 14 | import com.bumptech.glide.request.RequestOptions 15 | import com.bumptech.glide.signature.ObjectKey 16 | import java.io.InputStream 17 | 18 | @GlideModule 19 | class SaikouGlideApp : AppGlideModule(){ 20 | @SuppressLint("CheckResult") 21 | override fun applyOptions(context: Context, builder: GlideBuilder) { 22 | super.applyOptions(context, builder) 23 | builder.apply { 24 | RequestOptions() 25 | .diskCacheStrategy(DiskCacheStrategy.ALL) 26 | .signature(ObjectKey(System.currentTimeMillis().toShort())) 27 | } 28 | } 29 | 30 | // Needed for DOH 31 | // https://stackoverflow.com/a/61634041 32 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 33 | registry.replace( 34 | GlideUrl::class.java, 35 | InputStream::class.java, 36 | OkHttpUrlLoader.Factory(okHttpClient) 37 | ) 38 | super.registerComponents(context, glide, registry) 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/Idiosyncrasy.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST", "DEPRECATION") 2 | 3 | package ani.saikou.others 4 | 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | import java.io.Serializable 9 | 10 | inline fun Bundle.getSerialized(key: String): T? { 11 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 12 | this.getSerializable(key, T::class.java) 13 | else 14 | this.getSerializable(key) as? T 15 | } 16 | 17 | inline fun Intent.getSerialized(key: String): T? { 18 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 19 | this.getSerializableExtra(key, T::class.java) 20 | else 21 | this.getSerializableExtra(key) as? T 22 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/MalSyncBackup.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.others 2 | 3 | import ani.saikou.client 4 | import ani.saikou.parsers.ShowResponse 5 | import ani.saikou.tryWithSuspend 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | object MalSyncBackup { 10 | @Serializable 11 | data class MalBackUpSync( 12 | @SerialName("Pages") val pages: Map>? = null 13 | ) 14 | 15 | @Serializable 16 | data class Page( 17 | val identifier: String, 18 | val title: String, 19 | val url: String? = null, 20 | val image: String? = null, 21 | val active: Boolean? = null, 22 | ) 23 | 24 | suspend fun get(id: Int, name: String, dub: Boolean = false): ShowResponse? { 25 | return tryWithSuspend { 26 | val json = 27 | client.get("https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/anilist/anime/$id.json") 28 | if (json.text != "404: Not Found") 29 | json.parsed().pages?.get(name)?.forEach { 30 | val page = it.value 31 | val isDub = page.title.lowercase().replace(" ", "").endsWith("(dub)") 32 | val slug = if (dub == isDub) page.identifier else null 33 | if (slug != null && page.active == true && page.url != null) { 34 | val url = when(name){ 35 | "Gogoanime" -> slug 36 | "Tenshi" -> slug 37 | else -> page.url 38 | } 39 | return@tryWithSuspend ShowResponse(page.title, url, page.image ?: "") 40 | } 41 | } 42 | return@tryWithSuspend null 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/others/ResettableTimer.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.others 2 | 3 | import java.util.* 4 | import java.util.concurrent.atomic.* 5 | 6 | class ResettableTimer { 7 | var resetLock = AtomicBoolean(false) 8 | var timer = Timer() 9 | fun reset(timerTask: TimerTask, delay: Long) { 10 | if (!resetLock.getAndSet(true)) { 11 | timer.cancel() 12 | timer.purge() 13 | timer = Timer() 14 | timer.schedule(object : TimerTask() { 15 | override fun run() { 16 | if (!resetLock.getAndSet(true)) { 17 | timerTask.run() 18 | timer.cancel() 19 | timer.purge() 20 | resetLock.set(false) 21 | } 22 | } 23 | }, delay) 24 | resetLock.set(false) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/parsers/AnimeSources.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.parsers 2 | 3 | import ani.saikou.Lazier 4 | import ani.saikou.lazyList 5 | import ani.saikou.parsers.anime.* 6 | 7 | object AnimeSources : WatchSources() { 8 | override val list: List> = lazyList( 9 | "AllAnime" to ::AllAnime, 10 | "Gogo" to ::Gogo, 11 | "Zoro" to ::Zoro, 12 | "Marin" to ::Marin, 13 | "AnimePahe" to ::AnimePahe, 14 | "Monoschinos" to ::Monoschinos, 15 | "Animefenix" to ::Animefenix, 16 | "Jkanime" to ::Jkanime, 17 | "AnimeLatinoHD" to ::Animelatinohd, 18 | "Animeflv" to ::TioAnime, 19 | "ConsumeBili" to ::ConsumeBili 20 | ) 21 | } 22 | 23 | object HAnimeSources : WatchSources() { 24 | val aList: List> = lazyList( 25 | "HentaiMama" to ::HentaiMama, 26 | "Haho" to ::Haho, 27 | "HentaiStream" to ::HentaiStream, 28 | "HentaiFF" to ::HentaiFF, 29 | "Monoschinos" to ::Monoschinos, 30 | "Animefenix" to ::Animefenix, 31 | "Jkanime" to ::Jkanime, 32 | ) 33 | 34 | override val list = listOf(aList,AnimeSources.list).flatten() 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/parsers/MangaSources.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.parsers 2 | 3 | import ani.saikou.Lazier 4 | import ani.saikou.lazyList 5 | import ani.saikou.parsers.manga.* 6 | 7 | object MangaSources : MangaReadSources() { 8 | override val list: List> = lazyList( 9 | "MangaKakalot" to ::MangaKakalot, 10 | "MangaBuddy" to ::MangaBuddy, 11 | "MangaPill" to ::MangaPill, 12 | "MangaDex" to ::MangaDex, 13 | "MangaReaderTo" to ::MangaReaderTo, 14 | "AllAnime" to ::AllAnime, 15 | "Toonily" to ::Toonily, 16 | // "MangaHub" to ::MangaHub, 17 | "MangaKatana" to ::MangaKatana, 18 | "Manga4Life" to ::Manga4Life, 19 | "MangaRead" to ::MangaRead, 20 | "ComickFun" to ::ComickFun, 21 | ) 22 | } 23 | 24 | object HMangaSources : MangaReadSources() { 25 | val aList: List> = lazyList( 26 | "NineHentai" to ::NineHentai, 27 | "Manhwa18" to ::Manhwa18, 28 | "NHentai" to ::NHentai, 29 | ) 30 | override val list = listOf(aList,MangaSources.list).flatten() 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/parsers/anime/HentaiStream.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.parsers.anime 2 | 3 | class HentaiStream: HentaiFF() { 4 | override val name: String = "Hentai Stream" 5 | override val saveName: String = "hentai_stream" 6 | override val hostUrl = "https://hentaistream.moe" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/parsers/anime/extractors/FPlayer.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.parsers.anime.extractors 2 | 3 | import ani.saikou.asyncMap 4 | import ani.saikou.client 5 | import ani.saikou.getSize 6 | import ani.saikou.parsers.* 7 | import kotlinx.serialization.Serializable 8 | 9 | class FPlayer(override val server: VideoServer) : VideoExtractor() { 10 | 11 | override suspend fun extract(): VideoContainer { 12 | val url = server.embed.url 13 | val apiLink = url.replace("/v/", "/api/source/") 14 | try { 15 | val json = client.post(apiLink, referer = url).parsed() 16 | if (json.success) { 17 | return VideoContainer(json.data?.asyncMap { 18 | Video( 19 | it.label.replace("p", "").toIntOrNull(), 20 | VideoType.CONTAINER, 21 | it.file, 22 | getSize(it.file) 23 | ) 24 | }?: listOf()) 25 | } 26 | 27 | } catch (e: Exception) {} 28 | return VideoContainer(listOf()) 29 | } 30 | 31 | @Serializable 32 | private data class Data( 33 | val file: String, 34 | val label: String 35 | ) 36 | 37 | @Serializable 38 | private data class Json( 39 | val success: Boolean, 40 | val data: List? 41 | ) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/ani/saikou/parsers/anime/extractors/PStream.kt: -------------------------------------------------------------------------------- 1 | package ani.saikou.parsers.anime.extractors 2 | 3 | import android.util.Base64 4 | import ani.saikou.FileUrl 5 | import ani.saikou.Mapper 6 | import ani.saikou.client 7 | import ani.saikou.findBetween 8 | import ani.saikou.parsers.* 9 | import kotlinx.serialization.json.JsonObject 10 | import kotlinx.serialization.json.jsonPrimitive 11 | 12 | class PStream(override val server: VideoServer) : VideoExtractor() { 13 | 14 | val link = "https://www.pstream.net/u/player-script" 15 | val regex = Regex("""\)\)\}\("(.+)"\),n=.*file:.\.(.+),tracks:""") 16 | 17 | val headers = mapOf( 18 | "accept" to "*/*", 19 | "content-type" to "application/json", 20 | "accept-language" to "*/*" 21 | ) 22 | 23 | override suspend fun extract(): VideoContainer { 24 | val res = client.get(server.embed.url, headers).text 25 | val jslink = res.findBetween("