Add extension: Jellyfin (#1076)
Co-authored-by: jmir1 <jhmiramon@gmail.com>
This commit is contained in:
2
src/all/jellyfin/AndroidManifest.xml
Normal file
2
src/all/jellyfin/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.animeextension" />
|
16
src/all/jellyfin/build.gradle
Normal file
16
src/all/jellyfin/build.gradle
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Jellyfin'
|
||||||
|
pkgNameSuffix = 'all.jellyfin'
|
||||||
|
extClass = '.Jellyfin'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '13'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly libs.bundles.coroutines
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/all/jellyfin/res/web_hi_res_512.png
Normal file
BIN
src/all/jellyfin/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
@ -0,0 +1,168 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
object JFConstants {
|
||||||
|
const val APIKEY_KEY = "api_key"
|
||||||
|
const val USERID_KEY = "user_id"
|
||||||
|
const val USERNAME_TITLE = "Username"
|
||||||
|
const val USERNAME_KEY = "username"
|
||||||
|
const val PASSWORD_TITLE = "Password"
|
||||||
|
const val PASSWORD_KEY = "password"
|
||||||
|
const val HOSTURL_TITLE = "Host URL"
|
||||||
|
const val HOSTURL_KEY = "host_url"
|
||||||
|
const val MEDIALIB_KEY = "library_pref"
|
||||||
|
const val MEDIALIB_TITLE = "Select Media Library"
|
||||||
|
|
||||||
|
const val HOSTURL_DEFAULT = "http://127.0.0.1:8096"
|
||||||
|
|
||||||
|
fun getPrefApiKey(preferences: SharedPreferences): String? = preferences.getString(
|
||||||
|
APIKEY_KEY, null
|
||||||
|
)
|
||||||
|
fun getPrefUserId(preferences: SharedPreferences): String? = preferences.getString(
|
||||||
|
USERID_KEY, null
|
||||||
|
)
|
||||||
|
fun getPrefHostUrl(preferences: SharedPreferences): String = preferences.getString(
|
||||||
|
HOSTURL_KEY, HOSTURL_DEFAULT
|
||||||
|
)!!
|
||||||
|
fun getPrefUsername(preferences: SharedPreferences): String = preferences.getString(
|
||||||
|
USERNAME_KEY, ""
|
||||||
|
)!!
|
||||||
|
fun getPrefPassword(preferences: SharedPreferences): String = preferences.getString(
|
||||||
|
PASSWORD_KEY, ""
|
||||||
|
)!!
|
||||||
|
fun getPrefParentId(preferences: SharedPreferences): String = preferences.getString(
|
||||||
|
MEDIALIB_KEY, ""
|
||||||
|
)!!
|
||||||
|
|
||||||
|
const val PREF_AUDIO_KEY = "preferred_audioLang"
|
||||||
|
const val PREF_AUDIO_TITLE = "Preferred audio language"
|
||||||
|
const val PREF_SUB_KEY = "preferred_subLang"
|
||||||
|
const val PREF_SUB_TITLE = "Preferred sub language"
|
||||||
|
|
||||||
|
val QUALITIES_LIST = arrayOf(
|
||||||
|
Quality(480, 360, 292000, 128000, "360p - 420 kbps"),
|
||||||
|
Quality(854, 480, 528000, 192000, "480p - 720 kbps"),
|
||||||
|
Quality(854, 480, 1308000, 192000, "480p - 1.5 Mbps"),
|
||||||
|
Quality(854, 480, 2808000, 192000, "480p - 3 Mbps"),
|
||||||
|
Quality(1280, 720, 3808000, 192000, "720p - 4 Mbps"),
|
||||||
|
Quality(1280, 720, 5808000, 192000, "720p - 6 Mbps"),
|
||||||
|
Quality(1280, 720, 7808000, 192000, "720p - 8 Mbps"),
|
||||||
|
Quality(1920, 1080, 9808000, 192000, "1080p - 10 Mbps"),
|
||||||
|
Quality(1920, 1080, 14808000, 192000, "1080p - 15 Mbps"),
|
||||||
|
Quality(1920, 1080, 19808000, 192000, "1080p - 20 Mbps"),
|
||||||
|
Quality(1920, 1080, 39808000, 192000, "1080p - 40 Mbps"),
|
||||||
|
Quality(1920, 1080, 59808000, 192000, "1080p - 60 Mbps"),
|
||||||
|
Quality(3840, 2160, 80000000, 192000, "4K - 80 Mbps"),
|
||||||
|
Quality(3840, 2160, 120000000, 192000, "4K - 120 Mbps")
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Quality(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val videoBitrate: Int,
|
||||||
|
val audioBitrate: Int,
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
val PREF_VALUES = arrayOf(
|
||||||
|
"aar", "abk", "ace", "ach", "ada", "ady", "afh", "afr", "ain", "aka", "akk", "ale", "alt", "amh", "ang", "anp", "apa",
|
||||||
|
"ara", "arc", "arg", "arn", "arp", "arw", "asm", "ast", "ath", "ava", "ave", "awa", "aym", "aze", "bai", "bak", "bal",
|
||||||
|
"bam", "ban", "bas", "bej", "bel", "bem", "ben", "ber", "bho", "bik", "bin", "bis", "bla", "bod", "bos", "bra", "bre",
|
||||||
|
"bua", "bug", "bul", "byn", "cad", "car", "cat", "ceb", "ces", "cha", "chb", "che", "chg", "chk", "chm", "chn", "cho",
|
||||||
|
"chp", "chr", "chu", "chv", "chy", "cnr", "cop", "cor", "cos", "cre", "crh", "csb", "cym", "dak", "dan", "dar", "del",
|
||||||
|
"den", "deu", "dgr", "din", "div", "doi", "dsb", "dua", "dum", "dyu", "dzo", "efi", "egy", "eka", "ell", "elx", "eng",
|
||||||
|
"enm", "epo", "est", "eus", "ewe", "ewo", "fan", "fao", "fas", "fat", "fij", "fil", "fin", "fiu", "fon", "fra", "frm",
|
||||||
|
"fro", "frr", "frs", "fry", "ful", "fur", "gaa", "gay", "gba", "gez", "gil", "gla", "gle", "glg", "glv", "gmh", "goh",
|
||||||
|
"gon", "gor", "got", "grb", "grc", "grn", "gsw", "guj", "gwi", "hai", "hat", "hau", "haw", "heb", "her", "hil", "hin",
|
||||||
|
"hit", "hmn", "hmo", "hrv", "hsb", "hun", "hup", "hye", "iba", "ibo", "ido", "iii", "ijo", "iku", "ile", "ilo", "ina",
|
||||||
|
"inc", "ind", "inh", "ipk", "isl", "ita", "jav", "jbo", "jpn", "jpr", "jrb", "kaa", "kab", "kac", "kal", "kam", "kan",
|
||||||
|
"kar", "kas", "kat", "kau", "kaw", "kaz", "kbd", "kha", "khm", "kho", "kik", "kin", "kir", "kmb", "kok", "kom", "kon",
|
||||||
|
"kor", "kos", "kpe", "krc", "krl", "kru", "kua", "kum", "kur", "kut", "lad", "lah", "lam", "lao", "lat", "lav", "lez",
|
||||||
|
"lim", "lin", "lit", "lol", "loz", "ltz", "lua", "lub", "lug", "lui", "lun", "luo", "lus", "mad", "mag", "mah", "mai",
|
||||||
|
"mak", "mal", "man", "mar", "mas", "mdf", "mdr", "men", "mga", "mic", "min", "mkd", "mkh", "mlg", "mlt", "mnc", "mni",
|
||||||
|
"moh", "mon", "mos", "mri", "msa", "mus", "mwl", "mwr", "mya", "myv", "nah", "nap", "nau", "nav", "nbl", "nde", "ndo",
|
||||||
|
"nds", "nep", "new", "nia", "nic", "niu", "nld", "nno", "nob", "nog", "non", "nor", "nqo", "nso", "nub", "nwc", "nya",
|
||||||
|
"nym", "nyn", "nyo", "nzi", "oci", "oji", "ori", "orm", "osa", "oss", "ota", "oto", "pag", "pal", "pam", "pan", "pap",
|
||||||
|
"pau", "peo", "phn", "pli", "pol", "pon", "por", "pro", "pus", "que", "raj", "rap", "rar", "roh", "rom", "ron", "run",
|
||||||
|
"rup", "rus", "sad", "sag", "sah", "sam", "san", "sas", "sat", "scn", "sco", "sel", "sga", "shn", "sid", "sin", "slk",
|
||||||
|
"slv", "sma", "sme", "smj", "smn", "smo", "sms", "sna", "snd", "snk", "sog", "som", "son", "sot", "spa", "sqi", "srd",
|
||||||
|
"srn", "srp", "srr", "ssw", "suk", "sun", "sus", "sux", "swa", "swe", "syc", "syr", "tah", "tai", "tam", "tat", "tel",
|
||||||
|
"tem", "ter", "tet", "tgk", "tgl", "tha", "tig", "tir", "tiv", "tkl", "tlh", "tli", "tmh", "tog", "ton", "tpi", "tsi",
|
||||||
|
"tsn", "tso", "tuk", "tum", "tup", "tur", "tvl", "twi", "tyv", "udm", "uga", "uig", "ukr", "umb", "urd", "uzb", "vai",
|
||||||
|
"ven", "vie", "vol", "vot", "wal", "war", "was", "wen", "wln", "wol", "xal", "xho", "yao", "yap", "yid", "yor", "zap",
|
||||||
|
"zbl", "zen", "zgh", "zha", "zho", "zul", "zun", "zza"
|
||||||
|
)
|
||||||
|
|
||||||
|
val PREF_ENTRIES = arrayOf(
|
||||||
|
"Qafaraf; ’Afar Af; Afaraf; Qafar af", "Аҧсуа бызшәа Aƥsua bızšwa; Аҧсшәа Aƥsua", "بهسا اچيه", "Lwo", "Dangme",
|
||||||
|
"Адыгабзэ; Кӏахыбзэ", "El-Afrihili", "Afrikaans", "アイヌ・イタㇰ Ainu-itak", "Akan", "𒀝𒅗𒁺𒌑", "Уна́ӈам тунуу́; Унаӈан умсуу",
|
||||||
|
"Алтай тили", "አማርኛ Amârıñâ", "Ænglisc; Anglisc; Englisc", "Angika", "Apache languages", "العَرَبِيَّة al'Arabiyyeẗ",
|
||||||
|
"Official Aramaic (700–300 BCE); Imperial Aramaic (700–300 BCE)", "aragonés", "Mapudungun; Mapuche", "Hinónoʼeitíít",
|
||||||
|
"Lokono", "অসমীয়া", "Asturianu; Llïonés", "Athapascan languages", "Магӏарул мацӏ; Авар мацӏ", "Avestan", "अवधी",
|
||||||
|
"Aymar aru", "Azərbaycan dili; آذربایجان دیلی; Азәрбајҹан дили", "Bamiléké", "Башҡорт теле; Başqort tele",
|
||||||
|
"بلوچی", "ߓߊߡߊߣߊߣߞߊߣ", "ᬪᬵᬱᬩᬮᬶ; ᬩᬲᬩᬮᬶ; Basa Bali", "Mbene; Ɓasaá", "Bidhaawyeet", "Беларуская мова Belaruskaâ mova",
|
||||||
|
"Chibemba", "বাংলা Bāŋlā", "Tamaziɣt; Tamazight; ⵜⴰⵎⴰⵣⵉⵖⵜ; ⵝⴰⵎⴰⵣⵉⵗⵝ; ⵜⴰⵎⴰⵣⵉⵗⵜ", "भोजपुरी", "Bikol", "Ẹ̀dó",
|
||||||
|
"Bislama", "ᓱᖽᐧᖿ", "བོད་སྐད་ Bodskad; ལྷ་སའི་སྐད་ Lhas'iskad", "bosanski", "Braj", "Brezhoneg", "буряад хэлэн",
|
||||||
|
"ᨅᨔ ᨕᨘᨁᨗ", "български език bălgarski ezik", "ብሊና; ብሊን", "Hasí:nay", "Kari'nja", "català,valencià", "Sinugbuanong Binisayâ",
|
||||||
|
"čeština; český jazyk", "Finu' Chamoru", "Muysccubun", "Нохчийн мотт; نَاخچیین موٓتت; ნახჩიე მუოთთ", "جغتای",
|
||||||
|
"Chuukese", "марий йылме", "chinuk wawa; wawa; chinook lelang; lelang", "Chahta'", "ᑌᓀᓱᒼᕄᓀ (Dënesųłiné)",
|
||||||
|
"ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ Tsalagi gawonihisdi", "Славе́нскїй ѧ҆зы́къ", "Чӑвашла", "Tsėhésenėstsestȯtse", "crnogorski / црногорски",
|
||||||
|
"ϯⲙⲉⲑⲣⲉⲙⲛ̀ⲭⲏⲙⲓ; ⲧⲙⲛ̄ⲧⲣⲙ̄ⲛ̄ⲕⲏⲙⲉ", "Kernowek", "Corsu; Lingua corsa", "Cree", "Къырымтатарджа; Къырымтатар тили; Ҡырымтатарҗа; Ҡырымтатар тили",
|
||||||
|
"Kaszëbsczi jãzëk", "Cymraeg; y Gymraeg", "Dakhótiyapi; Dakȟótiyapi", "dansk", "дарган мез", "Delaware", "Dene K'e",
|
||||||
|
"Deutsch", "Dogrib", "Thuɔŋjäŋ", "ދިވެހި; ދިވެހިބަސް Divehi", "𑠖𑠵𑠌𑠤𑠮; डोगरी; ڈوگرى", "Dolnoserbski; Dolnoserbšćina",
|
||||||
|
"Duala", "Dutch, Middle (ca. 1050–1350)", "Julakan", "རྫོང་ཁ་ Ĵoŋkha", "Efik", "Egyptian (Ancient)", "Ekajuk",
|
||||||
|
"Νέα Ελληνικά Néa Ellêniká", "Elamite", "English", "English, Middle (1100–1500)", "Esperanto", "eesti keel",
|
||||||
|
"euskara", "Èʋegbe", "Ewondo", "Fang", "føroyskt", "فارسی Fārsiy", "Mfantse; Fante; Fanti", "Na Vosa Vakaviti",
|
||||||
|
"Wikang Filipino", "suomen kieli", "Finno-Ugrian languages", "Fon gbè", "français", "françois; franceis", "Franceis; François; Romanz",
|
||||||
|
"Frasch; Fresk; Freesk; Friisk", "Oostfreesk; Plattdüütsk", "Frysk", "Fulfulde; Pulaar; Pular", "Furlan",
|
||||||
|
"Gã", "Basa Gayo", "Gbaya", "ግዕዝ", "Taetae ni Kiribati", "Gàidhlig", "Gaeilge", "galego", "Gaelg; Gailck", "Diutsch",
|
||||||
|
"Diutisk", "Gondi", "Bahasa Hulontalo", "Gothic", "Grebo", "Ἑλληνική", "Avañe'ẽ", "Schwiizerdütsch", "ગુજરાતી Gujarātī",
|
||||||
|
"Dinjii Zhu’ Ginjik", "X̱aat Kíl; X̱aadas Kíl; X̱aayda Kil; Xaad kil", "kreyòl ayisyen", "Harshen Hausa; هَرْشَن",
|
||||||
|
"ʻŌlelo Hawaiʻi", "עברית 'Ivriyþ", "Otjiherero", "Ilonggo", "हिन्दी Hindī", "𒉈𒅆𒇷", "lus Hmoob; lug Moob; lol Hmongb; 𖬇𖬰𖬞 𖬌𖬣𖬵",
|
||||||
|
"Hiri Motu", "hrvatski", "hornjoserbšćina", "magyar nyelv", "Na:tinixwe Mixine:whe'", "Հայերէն Hayerèn; Հայերեն Hayeren",
|
||||||
|
"Jaku Iban", "Asụsụ Igbo", "Ido", "ꆈꌠꉙ Nuosuhxop", "Ịjọ", "ᐃᓄᒃᑎᑐᑦ Inuktitut", "Interlingue; Occidental", "Pagsasao nga Ilokano; Ilokano",
|
||||||
|
"Interlingua (International Auxiliary Language Association)", "Indo-Aryan languages", "bahasa Indonesia",
|
||||||
|
"ГӀалгӀай мотт", "Iñupiaq", "íslenska", "italiano; lingua italiana", "ꦧꦱꦗꦮ / Basa Jawa", "la .lojban.", "日本語 Nihongo",
|
||||||
|
"Dzhidi", "عربية يهودية / ערבית יהודית", "Qaraqalpaq tili; Қарақалпақ тили", "Tamaziɣt Taqbaylit; Tazwawt",
|
||||||
|
"Jingpho", "Kalaallisut; Greenlandic", "Kamba", "ಕನ್ನಡ Kannađa", "Karen languages", "कॉशुर / كأشُر", "ქართული Kharthuli",
|
||||||
|
"Kanuri", "ꦧꦱꦗꦮ", "қазақ тілі qazaq tili; қазақша qazaqşa", "Адыгэбзэ (Къэбэрдейбзэ) Adıgăbză (Qăbărdeĭbză)",
|
||||||
|
"কা কতিয়েন খাশি", "ភាសាខ្មែរ Phiəsaakhmær", "Khotanese; Sakan", "Gĩkũyũ", "Ikinyarwanda", "кыргызча kırgızça; кыргыз тили kırgız tili",
|
||||||
|
"Kimbundu", "कोंकणी", "Коми кыв", "Kongo", "한국어 Han'gug'ô", "Kosraean", "Kpɛlɛwoo", "Къарачай-Малкъар тил; Таулу тил",
|
||||||
|
"karjal; kariela; karjala", "कुड़ुख़", "Kuanyama; Kwanyama", "къумукъ тил/qumuq til", "kurdî / کوردی", "Kutenai",
|
||||||
|
"Judeo-español", "بھارت کا", "Lamba", "ພາສາລາວ Phasalaw", "Lingua latīna", "Latviešu valoda", "Лезги чӏал",
|
||||||
|
"Lèmburgs", "Lingala", "lietuvių kalba", "Lomongo", "Lozi", "Lëtzebuergesch", "Cilubà / Tshiluba", "Kiluba",
|
||||||
|
"Luganda", "Cham'teela", "Chilunda", "Dholuo", "Mizo ṭawng", "Madhura", "मगही", "Kajin M̧ajeļ", "मैथिली; মৈথিলী",
|
||||||
|
"Basa Mangkasara' / ᨅᨔ ᨆᨀᨔᨑ", "മലയാളം Malayāļã", "Mandi'nka kango", "मराठी Marāţhī", "ɔl", "мокшень кяль",
|
||||||
|
"Mandar", "Mɛnde yia", "Gaoidhealg", "Míkmawísimk", "Baso Minang", "македонски јазик makedonski jazik", "Mon-Khmer languages",
|
||||||
|
"Malagasy", "Malti", "ᠮᠠᠨᠵᡠ ᡤᡳᠰᡠᠨ Manju gisun", "Manipuri", "Kanien’kéha", "монгол хэл mongol xel; ᠮᠣᠩᠭᠣᠯ ᠬᠡᠯᠡ",
|
||||||
|
"Mooré", "Te Reo Māori", "Bahasa Melayu", "Mvskoke", "mirandés; lhéngua mirandesa", "मारवाड़ी", "မြန်မာစာ Mrãmācā; မြန်မာစကား Mrãmākā:",
|
||||||
|
"эрзянь кель", "Nahuatl languages", "napulitano", "dorerin Naoero", "Diné bizaad; Naabeehó bizaad", "isiNdebele seSewula",
|
||||||
|
"siNdebele saseNyakatho", "ndonga", "Plattdütsch; Plattdüütsch", "नेपाली भाषा Nepālī bhāśā", "नेपाल भाषा; नेवाः भाय्",
|
||||||
|
"Li Niha", "Niger-Kordofanian languages", "ko e vagahau Niuē", "Nederlands; Vlaams", "norsk nynorsk", "norsk bokmål",
|
||||||
|
"Ногай тили", "Dǫnsk tunga; Norrœnt mál", "norsk", "N'Ko", "Sesotho sa Leboa", "لغات نوبية", "पुलां भाय्; पुलाङु नेपाल भाय्",
|
||||||
|
"Chichewa; Chinyanja", "Nyamwezi", "Nyankole", "Runyoro", "Nzima", "occitan; lenga d'òc", "Ojibwa", "ଓଡ଼ିଆ",
|
||||||
|
"Afaan Oromoo", "Wazhazhe ie / 𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟", "Ирон ӕвзаг Iron ævzag", "لسان عثمانى / lisân-ı Osmânî", "Otomian languages",
|
||||||
|
"Salitan Pangasinan", "Pārsīk; Pārsīg", "Amánung Kapampangan; Amánung Sísuan", "ਪੰਜਾਬੀ / پنجابی Pãjābī",
|
||||||
|
"Papiamentu", "a tekoi er a Belau", "Persian, Old (ca. 600–400 B.C.)", "𐤃𐤁𐤓𐤉𐤌 𐤊𐤍𐤏𐤍𐤉𐤌 Dabariym Kana'aniym",
|
||||||
|
"Pāli", "Język polski", "Pohnpeian", "português", "Provençal, Old (to 1500); Old Occitan (to 1500)", "پښتو Pax̌tow",
|
||||||
|
"Runa simi; kichwa simi; Nuna shimi", "राजस्थानी", "Vananga rapa nui", "Māori Kūki 'Āirani", "Rumantsch; Rumàntsch; Romauntsch; Romontsch",
|
||||||
|
"romani čhib", "limba română", "Ikirundi", "armãneashce; armãneashti; rrãmãneshti", "русский язык russkiĭ âzık",
|
||||||
|
"Sandaweeki", "yângâ tî sängö", "Сахалыы", "ארמית", "संस्कृतम् Sąskŕtam; 𑌸𑌂𑌸𑍍𑌕𑍃𑌤𑌮𑍍", "Sasak", "ᱥᱟᱱᱛᱟᱲᱤ", "Sicilianu",
|
||||||
|
"Braid Scots; Lallans", "Selkup", "Goídelc", "ၵႂၢမ်းတႆးယႂ်", "Sidaamu Afoo", "සිංහල Sĩhala", "slovenčina; slovenský jazyk",
|
||||||
|
"slovenski jezik; slovenščina", "Åarjelsaemien gïele", "davvisámegiella", "julevsámegiella", "anarâškielâ",
|
||||||
|
"Gagana faʻa Sāmoa", "sääʹmǩiõll", "chiShona", "سنڌي / सिन्धी / ਸਿੰਧੀ", "Sooninkanxanne", "Sogdian", "af Soomaali",
|
||||||
|
"Songhai languages", "Sesotho [southern]", "español; castellano", "Shqip", "sardu; limba sarda; lingua sarda",
|
||||||
|
"Sranan Tongo", "српски / srpski", "Seereer", "siSwati", "Kɪsukuma", "ᮘᮞ ᮞᮥᮔ᮪ᮓ / Basa Sunda", "Sosoxui", "𒅴𒂠",
|
||||||
|
"Kiswahili", "svenska", "Classical Syriac", "ܠܫܢܐ ܣܘܪܝܝܐ Lešānā Suryāyā", "Reo Tahiti; Reo Mā'ohi", "ภาษาไท; ภาษาไต",
|
||||||
|
"தமிழ் Tamił", "татар теле / tatar tele / تاتار", "తెలుగు Telugu", "KʌThemnɛ", "Terêna", "Lia-Tetun", "тоҷикӣ toçikī",
|
||||||
|
"Wikang Tagalog", "ภาษาไทย Phasathay", "ትግረ; ትግሬ; ኻሳ; ትግራይት", "ትግርኛ", "Tiv", "Tokelau", "Klingon; tlhIngan-Hol",
|
||||||
|
"Lingít", "Tamashek", "chiTonga", "lea faka-Tonga", "Tok Pisin", "Tsimshian", "Setswana", "Xitsonga", "Türkmençe / Түркменче / تورکمن تیلی تورکمنچ; türkmen dili / түркмен дили",
|
||||||
|
"chiTumbuka", "Tupi languages", "Türkçe", "Te Ggana Tuuvalu; Te Gagana Tuuvalu", "Twi", "тыва дыл", "удмурт кыл",
|
||||||
|
"Ugaritic", "ئۇيغۇرچە ; ئۇيغۇر تىلى", "Українська мова; Українська", "Úmbúndú", "اُردُو Urduw", "Oʻzbekcha / Ózbekça / ўзбекча / ئوزبېچه; oʻzbek tili / ўзбек тили / ئوبېک تیلی",
|
||||||
|
"ꕙꔤ", "Tshivenḓa", "Tiếng Việt", "Volapük", "vađđa ceeli", "Wolaitta; Wolaytta", "Winaray; Samareño; Lineyte-Samarnon; Binisayâ nga Winaray; Binisayâ nga Samar-Leyte; “Binisayâ nga Waray”",
|
||||||
|
"wá:šiw ʔítlu", "Serbsce / Serbski", "Walon", "Wolof", "Хальмг келн / Xaľmg keln", "isiXhosa", "Yao", "Yapese",
|
||||||
|
"ייִדיש; יידיש; אידיש Yidiš", "èdè Yorùbá", "Diidxazá/Dizhsa", "Blissymbols; Blissymbolics; Bliss", "Tuḍḍungiyya",
|
||||||
|
"ⵜⴰⵎⴰⵣⵉⵖⵜ ⵜⴰⵏⴰⵡⴰⵢⵜ", "Vahcuengh / 話僮", "中文 Zhōngwén; 汉语; 漢語 Hànyǔ", "isiZulu", "Shiwi'ma", "kirmanckî; dimilkî; kirdkî; zazakî"
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,603 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.text.InputType
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
class Jellyfin : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "Jellyfin"
|
||||||
|
|
||||||
|
override val lang = "all"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var baseUrl = JFConstants.getPrefHostUrl(preferences)
|
||||||
|
|
||||||
|
private var username = JFConstants.getPrefUsername(preferences)
|
||||||
|
private var password = JFConstants.getPrefPassword(preferences)
|
||||||
|
private var parentId = JFConstants.getPrefParentId(preferences)
|
||||||
|
private var apiKey = JFConstants.getPrefApiKey(preferences)
|
||||||
|
private var userId = JFConstants.getPrefUserId(preferences)
|
||||||
|
|
||||||
|
init {
|
||||||
|
login(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(new: Boolean, context: Context? = null): Boolean? {
|
||||||
|
if (apiKey == null || userId == null || new) {
|
||||||
|
baseUrl = JFConstants.getPrefHostUrl(preferences)
|
||||||
|
username = JFConstants.getPrefUsername(preferences)
|
||||||
|
password = JFConstants.getPrefPassword(preferences)
|
||||||
|
if (username.isEmpty() || password.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val (newKey, newUid) = runBlocking {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
JellyfinAuthenticator(preferences, baseUrl, client)
|
||||||
|
.login(username, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newKey != null && newUid != null) {
|
||||||
|
apiKey = newKey
|
||||||
|
userId = newUid
|
||||||
|
} else {
|
||||||
|
context?.let { Toast.makeText(it, "Login failed.", Toast.LENGTH_LONG).show() }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular Anime (is currently sorted by name instead of e.g. ratings)
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
if (parentId.isEmpty()) {
|
||||||
|
throw Exception("Select library in the extension settings.")
|
||||||
|
}
|
||||||
|
val startIndex = (page - 1) * 20
|
||||||
|
|
||||||
|
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("api_key", apiKey)
|
||||||
|
url.addQueryParameter("StartIndex", startIndex.toString())
|
||||||
|
url.addQueryParameter("Limit", "20")
|
||||||
|
url.addQueryParameter("Recursive", "true")
|
||||||
|
url.addQueryParameter("SortBy", "SortName")
|
||||||
|
url.addQueryParameter("SortOrder", "Ascending")
|
||||||
|
url.addQueryParameter("includeItemTypes", "Movie,Series,Season")
|
||||||
|
url.addQueryParameter("ImageTypeLimit", "1")
|
||||||
|
url.addQueryParameter("ParentId", parentId)
|
||||||
|
url.addQueryParameter("EnableImageTypes", "Primary")
|
||||||
|
|
||||||
|
return GET(url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
val (list, hasNext) = animeParse(response)
|
||||||
|
return AnimesPage(
|
||||||
|
list.sortedBy { it.title },
|
||||||
|
hasNext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Episodes
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val json = Json.decodeFromString<JsonObject>(response.body!!.string())
|
||||||
|
|
||||||
|
val episodeList = mutableListOf<SEpisode>()
|
||||||
|
|
||||||
|
// Is movie
|
||||||
|
if (json.containsKey("Type")) {
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
val id = json["Id"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
episode.episode_number = 1.0F
|
||||||
|
episode.name = json["Name"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
episode.setUrlWithoutDomain("/Users/$userId/Items/$id?api_key=$apiKey")
|
||||||
|
episodeList.add(episode)
|
||||||
|
} else {
|
||||||
|
val items = json["Items"]!!.jsonArray
|
||||||
|
|
||||||
|
for (item in items) {
|
||||||
|
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
val jsonObj = item.jsonObject
|
||||||
|
|
||||||
|
val id = jsonObj["Id"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
val epNum = if (jsonObj["IndexNumber"] == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
jsonObj["IndexNumber"]!!.jsonPrimitive.float
|
||||||
|
}
|
||||||
|
if (epNum != null) {
|
||||||
|
episode.episode_number = epNum
|
||||||
|
val formattedEpNum = if (floor(epNum) == ceil(epNum)) {
|
||||||
|
epNum.toInt().toString()
|
||||||
|
} else {
|
||||||
|
epNum.toString()
|
||||||
|
}
|
||||||
|
episode.name = "Episode $formattedEpNum: " + jsonObj["Name"]!!.jsonPrimitive.content
|
||||||
|
} else {
|
||||||
|
episode.episode_number = 0F
|
||||||
|
episode.name = jsonObj["Name"]!!.jsonPrimitive.content
|
||||||
|
}
|
||||||
|
|
||||||
|
episode.setUrlWithoutDomain("/Users/$userId/Items/$id?api_key=$apiKey")
|
||||||
|
episodeList.add(episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodeList.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animeParse(response: Response): AnimesPage {
|
||||||
|
val items = Json.decodeFromString<JsonObject>(response.body!!.string())["Items"]?.jsonArray
|
||||||
|
|
||||||
|
val animesList = mutableListOf<SAnime>()
|
||||||
|
|
||||||
|
if (items != null) {
|
||||||
|
for (item in items) {
|
||||||
|
val anime = SAnime.create()
|
||||||
|
val jsonObj = item.jsonObject
|
||||||
|
|
||||||
|
if (jsonObj["Type"]!!.jsonPrimitive.content == "Season") {
|
||||||
|
val seasonId = jsonObj["Id"]!!.jsonPrimitive.content
|
||||||
|
val seriesId = jsonObj["SeriesId"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
anime.setUrlWithoutDomain("/Shows/$seriesId/Episodes?api_key=$apiKey&SeasonId=$seasonId")
|
||||||
|
|
||||||
|
// Virtual if show doesn't have any sub-folders, i.e. no seasons
|
||||||
|
if (jsonObj["LocationType"]!!.jsonPrimitive.content == "Virtual") {
|
||||||
|
anime.title = jsonObj["SeriesName"]!!.jsonPrimitive.content
|
||||||
|
anime.thumbnail_url = "$baseUrl/Items/$seriesId/Images/Primary?api_key=$apiKey"
|
||||||
|
} else {
|
||||||
|
anime.title = jsonObj["SeriesName"]!!.jsonPrimitive.content + " " + jsonObj["Name"]!!.jsonPrimitive.content
|
||||||
|
anime.thumbnail_url = "$baseUrl/Items/$seasonId/Images/Primary?api_key=$apiKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If season doesn't have image, fallback to series image
|
||||||
|
if (jsonObj["ImageTags"].toString() == "{}") {
|
||||||
|
anime.thumbnail_url = "$baseUrl/Items/$seriesId/Images/Primary?api_key=$apiKey"
|
||||||
|
}
|
||||||
|
} else if (jsonObj["Type"]!!.jsonPrimitive.content == "Movie") {
|
||||||
|
val id = jsonObj["Id"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
anime.title = jsonObj["Name"]!!.jsonPrimitive.content
|
||||||
|
anime.thumbnail_url = "$baseUrl/Items/$id/Images/Primary?api_key=$apiKey"
|
||||||
|
|
||||||
|
anime.setUrlWithoutDomain("/Users/$userId/Items/$id?api_key=$apiKey")
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
animesList.add(anime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = (items?.size?.compareTo(20) ?: -1) >= 0
|
||||||
|
return AnimesPage(animesList, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video urls
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val item = Json.decodeFromString<JsonObject>(response.body!!.string())
|
||||||
|
val id = item["Id"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
|
val sessionResponse = client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/Items/$id/PlaybackInfo?userId=$userId&api_key=$apiKey"
|
||||||
|
)
|
||||||
|
).execute()
|
||||||
|
val sessionJson = Json.decodeFromString<JsonObject>(sessionResponse.body!!.string())
|
||||||
|
val sessionId = sessionJson["PlaySessionId"]!!.jsonPrimitive.content
|
||||||
|
val mediaStreams = sessionJson["MediaSources"]!!.jsonArray[0].jsonObject["MediaStreams"]?.jsonArray
|
||||||
|
|
||||||
|
val subtitleList = mutableListOf<Track>()
|
||||||
|
|
||||||
|
val prefSub = preferences.getString(JFConstants.PREF_SUB_KEY, "eng")!!
|
||||||
|
val prefAudio = preferences.getString(JFConstants.PREF_AUDIO_KEY, "jpn")!!
|
||||||
|
|
||||||
|
var audioIndex = 1
|
||||||
|
var subIndex: Int? = null
|
||||||
|
var width = 1920
|
||||||
|
var height = 1080
|
||||||
|
|
||||||
|
// Get subtitle streams and audio index
|
||||||
|
if (mediaStreams != null) {
|
||||||
|
for (media in mediaStreams) {
|
||||||
|
val index = media.jsonObject["Index"]!!.jsonPrimitive.int
|
||||||
|
val codec = media.jsonObject["Codec"]!!.jsonPrimitive.content
|
||||||
|
val lang = media.jsonObject["Language"]
|
||||||
|
val supportsExternalStream = media.jsonObject["SupportsExternalStream"]!!.jsonPrimitive.boolean
|
||||||
|
|
||||||
|
val type = media.jsonObject["Type"]!!.jsonPrimitive.content
|
||||||
|
if (type == "Subtitle" && supportsExternalStream) {
|
||||||
|
val subUrl = "$baseUrl/Videos/$id/$id/Subtitles/$index/0/Stream.$codec?api_key=$apiKey"
|
||||||
|
// TODO: add ttf files in media attachment (if possible)
|
||||||
|
val title = media.jsonObject["DisplayTitle"]!!.jsonPrimitive.content
|
||||||
|
if (lang != null) {
|
||||||
|
if (lang.jsonPrimitive.content == prefSub) {
|
||||||
|
subtitleList.add(
|
||||||
|
0,
|
||||||
|
Track(subUrl, title)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
subtitleList.add(
|
||||||
|
Track(subUrl, title)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subtitleList.add(
|
||||||
|
Track(subUrl, title)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (type == "Subtitle") {
|
||||||
|
if (lang != null) {
|
||||||
|
if (lang.jsonPrimitive.content == prefSub) {
|
||||||
|
subIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == "Audio") {
|
||||||
|
if (lang != null) {
|
||||||
|
if (lang.jsonPrimitive.content == prefAudio) {
|
||||||
|
audioIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.jsonObject["Type"]!!.jsonPrimitive.content == "Video") {
|
||||||
|
width = media.jsonObject["Width"]!!.jsonPrimitive.int
|
||||||
|
height = media.jsonObject["Height"]!!.jsonPrimitive.int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over qualities
|
||||||
|
for (quality in JFConstants.QUALITIES_LIST) {
|
||||||
|
if (width < quality.width && height < quality.height) {
|
||||||
|
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
|
||||||
|
videoList.add(Video(url, "Best", url))
|
||||||
|
|
||||||
|
return videoList.reversed()
|
||||||
|
} else {
|
||||||
|
val url = "$baseUrl/videos/$id/main.m3u8".toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("api_key", apiKey)
|
||||||
|
url.addQueryParameter("VideoCodec", "h264")
|
||||||
|
url.addQueryParameter("AudioCodec", "aac,mp3")
|
||||||
|
url.addQueryParameter("AudioStreamIndex", audioIndex.toString())
|
||||||
|
subIndex?.let { url.addQueryParameter("SubtitleStreamIndex", it.toString()) }
|
||||||
|
url.addQueryParameter("VideoCodec", "h264")
|
||||||
|
url.addQueryParameter("VideoCodec", "h264")
|
||||||
|
url.addQueryParameter(
|
||||||
|
"VideoBitrate", quality.videoBitrate.toString()
|
||||||
|
)
|
||||||
|
url.addQueryParameter(
|
||||||
|
"AudioBitrate", quality.audioBitrate.toString()
|
||||||
|
)
|
||||||
|
url.addQueryParameter("PlaySessionId", sessionId)
|
||||||
|
url.addQueryParameter("TranscodingMaxAudioChannels", "6")
|
||||||
|
url.addQueryParameter("RequireAvc", "false")
|
||||||
|
url.addQueryParameter("SegmentContainer", "ts")
|
||||||
|
url.addQueryParameter("MinSegments", "1")
|
||||||
|
url.addQueryParameter("BreakOnNonKeyFrames", "true")
|
||||||
|
url.addQueryParameter("h264-profile", "high,main,baseline,constrainedbaseline")
|
||||||
|
url.addQueryParameter("h264-level", "51")
|
||||||
|
url.addQueryParameter("h264-deinterlace", "true")
|
||||||
|
url.addQueryParameter("TranscodeReasons", "VideoCodecNotSupported,AudioCodecNotSupported,ContainerBitrateExceedsLimit")
|
||||||
|
|
||||||
|
videoList.add(Video(url.toString(), quality.description, url.toString(), subtitleTracks = subtitleList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "$baseUrl/Videos/$id/stream?static=True&api_key=$apiKey"
|
||||||
|
videoList.add(Video(url, "Best", url))
|
||||||
|
|
||||||
|
return videoList.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// search
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response) = animeParse(response)
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
if (parentId.isEmpty()) {
|
||||||
|
throw Exception("Select library in the extension settings.")
|
||||||
|
}
|
||||||
|
if (query.isBlank()) {
|
||||||
|
throw Exception("Search query blank")
|
||||||
|
}
|
||||||
|
val startIndex = (page - 1) * 20
|
||||||
|
|
||||||
|
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("api_key", apiKey)
|
||||||
|
url.addQueryParameter("StartIndex", startIndex.toString())
|
||||||
|
url.addQueryParameter("Limit", "20")
|
||||||
|
url.addQueryParameter("Recursive", "true")
|
||||||
|
url.addQueryParameter("SortBy", "SortName")
|
||||||
|
url.addQueryParameter("SortOrder", "Ascending")
|
||||||
|
url.addQueryParameter("includeItemTypes", "Movie,Series,Season")
|
||||||
|
url.addQueryParameter("ImageTypeLimit", "1")
|
||||||
|
url.addQueryParameter("EnableImageTypes", "Primary")
|
||||||
|
url.addQueryParameter("ParentId", parentId)
|
||||||
|
url.addQueryParameter("SearchTerm", query)
|
||||||
|
|
||||||
|
return GET(url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details
|
||||||
|
|
||||||
|
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
|
val infoArr = anime.url.split("/").toTypedArray()
|
||||||
|
|
||||||
|
val id = if (infoArr[1] == "Users") {
|
||||||
|
infoArr[4].split("?").toTypedArray()[0]
|
||||||
|
} else {
|
||||||
|
infoArr[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "$baseUrl/Users/$userId/Items/$id".toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("api_key", apiKey)
|
||||||
|
url.addQueryParameter("fields", "Studios")
|
||||||
|
|
||||||
|
return GET(url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
|
val item = response.body?.let { Json.decodeFromString<JsonObject>(it.string()) }!!.jsonObject
|
||||||
|
|
||||||
|
val anime = SAnime.create()
|
||||||
|
|
||||||
|
anime.author = if (item["Studios"]!!.jsonArray.isEmpty()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
item["Studios"]!!.jsonArray[0].jsonObject["Name"]!!.jsonPrimitive.content
|
||||||
|
}
|
||||||
|
|
||||||
|
anime.description = item["Overview"]?.let {
|
||||||
|
Jsoup.parse(it.jsonPrimitive.content.replace("<br>", "br2n")).text().replace("br2n", "\n")
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
if (item["Genres"]!!.jsonArray.isEmpty()) {
|
||||||
|
anime.genre = ""
|
||||||
|
} else {
|
||||||
|
val genres = mutableListOf<String>()
|
||||||
|
|
||||||
|
for (genre in item["Genres"]!!.jsonArray) {
|
||||||
|
genres.add(genre.jsonPrimitive.content)
|
||||||
|
}
|
||||||
|
anime.genre = genres.joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
anime.status = item["Status"]?.let {
|
||||||
|
if (it.jsonPrimitive.content == "Ended") SAnime.COMPLETED else SAnime.COMPLETED
|
||||||
|
} ?: SAnime.UNKNOWN
|
||||||
|
|
||||||
|
return anime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
if (parentId.isEmpty()) {
|
||||||
|
throw Exception("Select library in the extension settings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val startIndex = (page - 1) * 20
|
||||||
|
|
||||||
|
val url = "$baseUrl/Users/$userId/Items".toHttpUrl().newBuilder()
|
||||||
|
|
||||||
|
url.addQueryParameter("api_key", apiKey)
|
||||||
|
url.addQueryParameter("StartIndex", startIndex.toString())
|
||||||
|
url.addQueryParameter("Limit", "20")
|
||||||
|
url.addQueryParameter("Recursive", "true")
|
||||||
|
url.addQueryParameter("SortBy", "DateCreated,SortName")
|
||||||
|
url.addQueryParameter("SortOrder", "Descending")
|
||||||
|
url.addQueryParameter("includeItemTypes", "Movie,Series,Season")
|
||||||
|
url.addQueryParameter("ImageTypeLimit", "1")
|
||||||
|
url.addQueryParameter("ParentId", parentId)
|
||||||
|
url.addQueryParameter("EnableImageTypes", "Primary")
|
||||||
|
|
||||||
|
return GET(url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) = animeParse(response)
|
||||||
|
|
||||||
|
// Filters - not used
|
||||||
|
|
||||||
|
// settings
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val mediaLibPref = medialibPreference(screen)
|
||||||
|
screen.addPreference(
|
||||||
|
screen.editTextPreference(
|
||||||
|
JFConstants.HOSTURL_KEY, JFConstants.HOSTURL_TITLE, JFConstants.HOSTURL_DEFAULT, baseUrl, false, "", mediaLibPref
|
||||||
|
)
|
||||||
|
)
|
||||||
|
screen.addPreference(
|
||||||
|
screen.editTextPreference(
|
||||||
|
JFConstants.USERNAME_KEY, JFConstants.USERNAME_TITLE, "", username, false, "", mediaLibPref
|
||||||
|
)
|
||||||
|
)
|
||||||
|
screen.addPreference(
|
||||||
|
screen.editTextPreference(
|
||||||
|
JFConstants.PASSWORD_KEY, JFConstants.PASSWORD_TITLE, "", password, true, "••••••••", mediaLibPref
|
||||||
|
)
|
||||||
|
)
|
||||||
|
screen.addPreference(mediaLibPref)
|
||||||
|
val subLangPref = ListPreference(screen.context).apply {
|
||||||
|
key = JFConstants.PREF_SUB_KEY
|
||||||
|
title = JFConstants.PREF_SUB_TITLE
|
||||||
|
entries = JFConstants.PREF_ENTRIES
|
||||||
|
entryValues = JFConstants.PREF_VALUES
|
||||||
|
setDefaultValue("eng")
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(subLangPref)
|
||||||
|
val audioLangPref = ListPreference(screen.context).apply {
|
||||||
|
key = JFConstants.PREF_AUDIO_KEY
|
||||||
|
title = JFConstants.PREF_AUDIO_TITLE
|
||||||
|
entries = JFConstants.PREF_ENTRIES
|
||||||
|
entryValues = JFConstants.PREF_VALUES
|
||||||
|
setDefaultValue("jpn")
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(audioLangPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class MediaLibPreference(context: Context) : ListPreference(context) {
|
||||||
|
abstract fun reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun medialibPreference(screen: PreferenceScreen) =
|
||||||
|
(
|
||||||
|
object : MediaLibPreference(screen.context) {
|
||||||
|
override fun reload() {
|
||||||
|
this.apply {
|
||||||
|
key = JFConstants.MEDIALIB_KEY
|
||||||
|
title = JFConstants.MEDIALIB_TITLE
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val mediaLibsResponse = client.newCall(
|
||||||
|
GET("$baseUrl/Users/$userId/Items?api_key=$apiKey")
|
||||||
|
).execute()
|
||||||
|
val mediaJson = mediaLibsResponse.body?.let { Json.decodeFromString<JsonObject>(it.string()) }?.get("Items")?.jsonArray
|
||||||
|
|
||||||
|
val entriesArray = mutableListOf<String>()
|
||||||
|
val entriesValueArray = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (mediaJson != null) {
|
||||||
|
for (media in mediaJson) {
|
||||||
|
entriesArray.add(media.jsonObject["Name"]!!.jsonPrimitive.content)
|
||||||
|
entriesValueArray.add(media.jsonObject["Id"]!!.jsonPrimitive.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = entriesArray.toTypedArray()
|
||||||
|
entryValues = entriesValueArray.toTypedArray()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
entries = emptyArray()
|
||||||
|
entryValues = emptyArray()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
parentId = entry
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).apply { reload() }
|
||||||
|
|
||||||
|
private fun PreferenceScreen.editTextPreference(key: String, title: String, default: String, value: String, isPassword: Boolean = false, placeholder: String, mediaLibPref: MediaLibPreference): EditTextPreference {
|
||||||
|
return EditTextPreference(context).apply {
|
||||||
|
this.key = key
|
||||||
|
this.title = title
|
||||||
|
summary = if ((isPassword && value.isNotEmpty()) || (!isPassword && value.isEmpty())) {
|
||||||
|
placeholder
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
this.setDefaultValue(default)
|
||||||
|
dialogTitle = title
|
||||||
|
|
||||||
|
setOnBindEditTextListener {
|
||||||
|
it.inputType = if (isPassword) {
|
||||||
|
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
} else {
|
||||||
|
InputType.TYPE_CLASS_TEXT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
try {
|
||||||
|
val newValueString = newValue as String
|
||||||
|
val res = preferences.edit().putString(key, newValueString).commit()
|
||||||
|
summary = if ((isPassword && newValueString.isNotEmpty()) || (!isPassword && newValueString.isEmpty())) {
|
||||||
|
placeholder
|
||||||
|
} else {
|
||||||
|
newValueString
|
||||||
|
}
|
||||||
|
val loginRes = login(true, context)
|
||||||
|
if (loginRes == true) {
|
||||||
|
mediaLibPref.reload()
|
||||||
|
}
|
||||||
|
res
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.all.jellyfin
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Build
|
||||||
|
import eu.kanade.tachiyomi.AppInfo
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
|
class JellyfinAuthenticator(
|
||||||
|
private val preferences: SharedPreferences,
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
) {
|
||||||
|
fun login(username: String, password: String): Pair<String?, String?> {
|
||||||
|
return try {
|
||||||
|
val authResult = authenticateWithPassword(username, password)
|
||||||
|
?: throw Exception()
|
||||||
|
val key = authResult["AccessToken"]!!.jsonPrimitive.content
|
||||||
|
val userId = authResult["SessionInfo"]!!.jsonObject["UserId"]!!.jsonPrimitive.content
|
||||||
|
saveLogin(key, userId)
|
||||||
|
Pair(key, userId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Pair(null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authenticateWithPassword(username: String, password: String): JsonObject? {
|
||||||
|
var deviceId = getPrefDeviceId()
|
||||||
|
if (deviceId.isNullOrEmpty()) {
|
||||||
|
deviceId = getRandomString()
|
||||||
|
setPrefDeviceId(deviceId)
|
||||||
|
}
|
||||||
|
val aniyomiVersion = AppInfo.getVersionName()
|
||||||
|
val androidVersion = Build.VERSION.RELEASE
|
||||||
|
val authHeader = Headers.headersOf(
|
||||||
|
"X-Emby-Authorization",
|
||||||
|
"MediaBrowser Client=\"$CLIENT\", Device=\"Android $androidVersion\", DeviceId=\"$deviceId\", Version=\"$aniyomiVersion\"",
|
||||||
|
)
|
||||||
|
val body = """
|
||||||
|
{"Username":"$username","Pw":"$password"}
|
||||||
|
""".trimIndent()
|
||||||
|
.toRequestBody("application/json".toMediaType())
|
||||||
|
val request = POST("$baseUrl/Users/authenticatebyname", headers = authHeader, body = body)
|
||||||
|
val response = client.newCall(request).execute().body?.string()
|
||||||
|
return response?.let { Json.decodeFromString<JsonObject>(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRandomString(): String {
|
||||||
|
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||||
|
return (1..172)
|
||||||
|
.map { allowedChars.random() }
|
||||||
|
.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLogin(key: String, userId: String) {
|
||||||
|
preferences.edit()
|
||||||
|
.putString(JFConstants.APIKEY_KEY, key)
|
||||||
|
.putString(JFConstants.USERID_KEY, userId)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPrefDeviceId(): String? = preferences.getString(
|
||||||
|
DEVICEID_KEY, null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun setPrefDeviceId(value: String) = preferences.edit().putString(
|
||||||
|
DEVICEID_KEY, value
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEVICEID_KEY = "device_id"
|
||||||
|
private const val CLIENT = "Aniyomi"
|
Reference in New Issue
Block a user