diff --git a/src/all/jellyfin/AndroidManifest.xml b/src/all/jellyfin/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/all/jellyfin/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/all/jellyfin/build.gradle b/src/all/jellyfin/build.gradle
new file mode 100644
index 000000000..3de49eca7
--- /dev/null
+++ b/src/all/jellyfin/build.gradle
@@ -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"
diff --git a/src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png b/src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..e81dcdc90
Binary files /dev/null and b/src/all/jellyfin/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png b/src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..534be2bd8
Binary files /dev/null and b/src/all/jellyfin/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png b/src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..9247871b3
Binary files /dev/null and b/src/all/jellyfin/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png b/src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c434ee3c4
Binary files /dev/null and b/src/all/jellyfin/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..a20ef126e
Binary files /dev/null and b/src/all/jellyfin/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/jellyfin/res/web_hi_res_512.png b/src/all/jellyfin/res/web_hi_res_512.png
new file mode 100644
index 000000000..e134595d1
Binary files /dev/null and b/src/all/jellyfin/res/web_hi_res_512.png differ
diff --git a/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/JFConstants.kt b/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/JFConstants.kt
new file mode 100644
index 000000000..f0224da11
--- /dev/null
+++ b/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/JFConstants.kt
@@ -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î"
+ )
+}
diff --git a/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/Jellyfin.kt b/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/Jellyfin.kt
new file mode 100644
index 000000000..db8b6b05f
--- /dev/null
+++ b/src/all/jellyfin/src/eu/kanade/tachiyomi/animeextension/all/jellyfin/Jellyfin.kt
@@ -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().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 {
+ val json = Json.decodeFromString(response.body!!.string())
+
+ val episodeList = mutableListOf()
+
+ // 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(response.body!!.string())["Items"]?.jsonArray
+
+ val animesList = mutableListOf()
+
+ 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