anime(aniplay): rewrite anime details and video list logic

This commit is contained in:
Swakshan
2025-04-27 19:20:52 +05:30
parent 2cbedc93de
commit 79ef1409b4

View File

@@ -6,7 +6,7 @@ const mangayomiSources = [{
"iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://aniplaynow.live/", "iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://aniplaynow.live/",
"typeSource": "single", "typeSource": "single",
"itemType": 1, "itemType": 1,
"version": "1.4.0", "version": "1.4.5",
"dateFormat": "", "dateFormat": "",
"dateFormatLocale": "", "dateFormatLocale": "",
"pkgPath": "anime/src/en/aniplay.js" "pkgPath": "anime/src/en/aniplay.js"
@@ -307,7 +307,7 @@ class DefaultExtension extends MProvider {
"Content-Type": "application/json", "Content-Type": "application/json",
} }
var response = await new Client().post(url, headers, body); var response = await this.client.post(url, headers, body);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw new Error("Error: " + response.statusText); throw new Error("Error: " + response.statusText);
@@ -319,6 +319,9 @@ class DefaultExtension extends MProvider {
async getDetail(url) { async getDetail(url) {
var anilistId = url var anilistId = url
if (url.includes("info/")) {
anilistId = url.substring(url.lastIndexOf("info/") + 5)
}
var animeData = await this.getAnimeDetails(anilistId) var animeData = await this.getAnimeDetails(anilistId)
@@ -329,39 +332,57 @@ class DefaultExtension extends MProvider {
throw new Error("Error: No data found for the given URL"); throw new Error("Error: No data found for the given URL");
} }
var user_provider = this.getPreference("aniplay_pref_provider_3"); var chapters = []
var choices = result var chaps = {}
if (user_provider !== "all") { for (var item of result) {
for (var ch of result) { var providerId = item["providerId"]
if (ch["providerId"] == user_provider) { // Hika has bunch of embeds, not stream url, so avoiding it for now
choices = [ch] if (providerId == "hika") continue
break; var episodes = item['episodes']
}
for (var episode of episodes) {
var id = episode.id
var number = episode.number.toString()
var chap = chaps.hasOwnProperty(number) ? chaps[number] : {}
var updatedAt = episode.hasOwnProperty("updatedAt") ? episode.updatedAt.toString() : null
var title = episode.hasOwnProperty("title") ? episode.title : ""
var isFiller = episode.hasOwnProperty("isFiller") ? episode.isFiller : false
var hasDub = episode.hasOwnProperty("hasDub") ? episode.hasDub : false
chap.title = title == "" ? chap.title : title
chap.isFiller = isFiller || chap.isFiller
chap.hasDub = hasDub || chap.hasDub
chap.updatedAt = updatedAt ?? chap.updatedAt
var prvds = chap.hasOwnProperty("prvds") ? chap["prvds"] : {}
prvds[providerId] = { anilistId, providerId, id, number, hasDub }
chap["prvds"] = prvds
chaps[number] = chap
} }
} }
var markFillers = this.getPreference("aniplay_pref_mark_filler")
for (const choice of choices) { for (var episodeNum in chaps) {
var user_mark_filler_ep = this.getPreference("aniplay_pref_mark_filler"); var chap = chaps[episodeNum]
var chapters = [] var title = chap.title
var epList = choice.episodes var dateUpload = chap.updatedAt
for (var ep of epList) { var scanlator = "SUB"
var title = ep.title if (chap.hasDub) {
var num = ep.number scanlator += ", DUB"
var isFiller = ep.isFiller
var scanlator = isFiller && user_mark_filler_ep ? `Filler` : null;
var dateUpload = "createdAt" in ep ? new Date(ep.createdAt) : Date.now()
dateUpload = dateUpload.valueOf().toString();
delete ep.img
delete ep.title
delete ep.description
delete ep.isFiller
var epUrl = `${anilistId}||${JSON.stringify(ep)}||${choice.providerId}`
chapters.push({ name: `E${num}: ${title}`, url: epUrl, dateUpload, scanlator })
} }
} var isFillers = chap.isFiller
if (markFillers && isFillers) {
scanlator = "FILLER, " + scanlator
}
var epData = JSON.stringify(chap["prvds"])
chapters.push({ name: `E${episodeNum}: ${title}`, url: epData, dateUpload, scanlator })
}
var format = animeData.format var format = animeData.format
if (format === "MOVIE") chapters[0].name = "Movie" if (format === "MOVIE") chapters[0].name = "Movie"
@@ -372,179 +393,84 @@ class DefaultExtension extends MProvider {
return animeData return animeData
} }
// Sorts streams based on user preference.
async sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice()
var pref = await this.getPreference("aniplay_pref_video_resolution");
for (var stream of streams) {
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams]
}
// Adds proxy to streams
async streamProxy(providerId, streams) {
var proxyBaseUrl = this.getPreference("aniplay_stream_proxy");
var slug = "/fetch?url="
var ref = "&ref="
if (providerId == "yuki") {
ref += "https://hianime.to"
} else if (providerId == "pahe") {
ref += "https://kwik.si"
} else if (providerId === "maze") {
ref += ""
} else if (providerId === "kuro") {
ref = ""
}
streams.forEach(stream => {
stream.url = proxyBaseUrl + slug + stream.url + ref;
stream.originalUrl = proxyBaseUrl + slug + stream.originalUrl + ref;
});
return streams
}
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url, providerId, hdr = {}) {
var streams = [{
url: url,
originalUrl: url,
quality: `Auto - ${providerId}`,
headers: hdr
}];
var doExtract = this.getPreference("aniplay_pref_extract_streams");
// Pahe only has auto
if (providerId === "pahe" || !doExtract) {
return streams;
}
const response = await new Client().get(url, hdr);
const body = response.body;
const lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
var m3u8Url = lines[i + 1].trim();
if (providerId === "yuki") {
var orginalUrl = url
m3u8Url = orginalUrl.replace("master.m3u8", m3u8Url)
}
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: `${resolution} - ${providerId}`,
headers: hdr
});
}
}
return streams
}
async getAnyStreams(result) {
var m3u8Url = result.sources[0].url
return await this.extractStreams(m3u8Url, "any");
}
async getYukiStreams(result) {
var m3u8Url = result.sources[0].url
var streams = await this.extractStreams(m3u8Url, "yuki");
var subtitles = []
result.subtitles.forEach(sub => {
var label = sub.label
if (label.indexOf("thumbnail") < 0) { // thumbnails shouldnt be included
subtitles.push({
"label": label,
"file": sub.url,
});
}
})
streams[0].subtitles = subtitles
return streams
}
async getPaheStreams(result) {
var m3u8Url = result.sources[0].url
var hdr = result.headers;
return await this.extractStreams(m3u8Url, "pahe", hdr);
}
async getMazeStreams(result) {
var m3u8Url = result.sources[0].url
var hdr = result.headers;
return await this.extractStreams(m3u8Url, "maze", hdr);
}
async getKuroStreams(result) {
var links = result.sources
var hdr = result.headers;
var streams = [];
for (var stream of links) {
var quality = stream.quality
quality = quality == "default" ? "auto" : quality
streams.push({
url: stream.url,
originalUrl: stream.url,
quality: `${quality} - kuro`,
headers: hdr
});
}
return streams
}
// For anime episode video list // For anime episode video list
async getVideoList(url) { async getVideoList(url) {
var urlSplits = url.split("||") var providerInfos = JSON.parse(url)
var anilistId = urlSplits[0]
var epData = JSON.parse(urlSplits[1])
var providerId = urlSplits[2]
var user_audio_type = this.getPreference("aniplay_pref_audio_type"); var pref_provider = this.getPreference("aniplay_pref_provider_4")
var subOrDub = epData.hasDub && user_audio_type === "dub" ? "dub" : "sub" var providers = Object.keys(providerInfos)
var slug = `watch/${anilistId}` if (pref_provider == "any") {
var body = [ //any = randomly choose one
anilistId, var randomIndex = Math.floor(Math.random() * providers.length);
providerId, providers = [providers[randomIndex]]
epData.id,
epData.number.toString(), } else if (pref_provider == "pahe") {
subOrDub //pahe = if "pahe" is available then chose it
] if (providerInfos.hasOwnProperty("pahe")) {
var result = await this.aniplayRequest(slug, body) providers = ["pahe"]
if (result === null) { }
} else if (pref_provider == "yuki") {
//yuki = if "yuki" is available then chose it
if (providerInfos.hasOwnProperty("yuki")) {
providers = ["yuki"]
}
}
var finalStreams = []
var user_audio_type = this.getPreference("aniplay_pref_audio_type_1")
for (var provider of providers) {
var streams = []
var providerInfo = providerInfos[provider]
var anilistId = providerInfo.anilistId
var providerId = providerInfo.providerId
var id = providerInfo.id
var number = providerInfo.number
var hasDub = providerInfo.hasDub
var audios = []
// if there "sub" is prefered or there are no preference then add sub
if (user_audio_type.includes("sub") || user_audio_type.length < 1) audios.push("sub")
if (hasDub && user_audio_type.includes("dub")) audios.push("dub")
var slug = `watch/${anilistId}`
for (var audio of audios) {
var body = [
anilistId,
providerId,
id,
number,
audio
]
var result = await this.aniplayRequest(slug, body)
if (result === null) {
continue
}
if (providerId == "yuki") {
// Yuki always has softsubs aka subtitles are seperate.
streams = await this.getYukiStreams(result, "soft" + audio)
} else if (providerId == "pahe") {
// Pahe always has hardsubs aka subtitles printed on video.
streams = await this.getPaheStreams(result, "hard" + audio)
} else {
continue
}
if (this.getPreference("aniplay_proxy")) streams = this.streamProxy(providerId, streams)
var sortedStreams = this.sortStreams(streams)
finalStreams = [...sortedStreams, ...finalStreams]
}
}
if (finalStreams.length < 1) {
throw new Error("Error: No data found for the given URL"); throw new Error("Error: No data found for the given URL");
} }
return finalStreams
var streams = []
if (providerId == "yuki") {
streams = await this.getYukiStreams(result)
} else if (providerId == "pahe") {
streams = await this.getPaheStreams(result)
} else if (providerId === "maze") {
streams = await this.getMazeStreams(result)
} else if (providerId === "kuro") {
streams = await this.getKuroStreams(result)
} else {
streams = await this.getAnyStreams(result) //If new provider found getAnyStreams will extract only the streams
}
if (this.getPreference("aniplay_proxy")) streams = await this.streamProxy(providerId, streams)
return await this.sortStreams(streams)
} }
getSourcePreferences() { getSourcePreferences() {
@@ -559,47 +485,47 @@ class DefaultExtension extends MProvider {
} }
}, },
{ {
"key": "aniplay_pref_title", key: "aniplay_pref_title",
"listPreference": { listPreference: {
"title": "Preferred Title", title: "Preferred Title",
"summary": "", summary: "",
"valueIndex": 0, valueIndex: 0,
"entries": ["Romaji", "English", "Native"], entries: ["Romaji", "English", "Native"],
"entryValues": ["romaji", "english", "native"], entryValues: ["romaji", "english", "native"],
} }
}, },
{ {
"key": "aniplay_pref_provider_3", key: "aniplay_pref_provider_4",
"listPreference": { listPreference: {
"title": "Preferred provider", title: "Preferred provider",
"summary": "", summary: "",
"valueIndex": 0, valueIndex: 1,
"entries": ["Any", "Yuki", "Pahe", "Maze", "Kuro"], entries: ["Any", "All", "Yuki", "Pahe"],
"entryValues": ["any", "yuki", "pahe", "maze", "kuro"], entryValues: ["any", "all", "yuki", "pahe"],
} }
}, { }, {
"key": "aniplay_pref_mark_filler", key: "aniplay_pref_mark_filler",
"switchPreferenceCompat": { switchPreferenceCompat: {
"title": "Mark filler episodes", title: "Mark filler episodes",
"summary": "Filler episodes will be marked with (F)", summary: "Filler episodes will be marked with (F)",
"value": false value: false
} }
}, },
{ {
"key": "aniplay_pref_audio_type", key: "aniplay_pref_audio_type_1",
"listPreference": { multiSelectListPreference: {
"title": "Preferred audio type", title: 'Preferred stream sub/dub type',
"summary": "Sub/Dub", summary: '',
"valueIndex": 0, values: ["sub"],
"entries": ["Sub", "Dub"], entries: ["Sub", "Dub"],
"entryValues": ["sub", "dub"], entryValues: ["sub", "dub"]
} }
}, { }, {
"key": "aniplay_pref_extract_streams", key: "aniplay_pref_extract_streams",
"switchPreferenceCompat": { switchPreferenceCompat: {
'title': 'Split stream into different quality streams', 'title': 'Split stream into different quality streams',
"summary": "Split stream Auto into 360p/720p/1080p", summary: "Split stream Auto into 360p/720p/1080p",
"value": true value: true
} }
}, { }, {
key: 'aniplay_pref_video_resolution', key: 'aniplay_pref_video_resolution',
@@ -631,8 +557,112 @@ class DefaultExtension extends MProvider {
] ]
} }
//------------ Extract keys --------------- // ----------- Stream manipulations -------
// Sorts streams based on user preference.
sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice()
var pref = this.getPreference("aniplay_pref_video_resolution");
for (var stream of streams) {
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams]
}
// Adds proxy to streams
streamProxy(providerId, streams) {
var proxyBaseUrl = this.getPreference("aniplay_stream_proxy");
var slug = "/fetch?url="
var ref = "&ref="
if (providerId == "yuki") {
ref += "https://hianime.to"
} else if (providerId == "pahe") {
ref += "https://kwik.si"
}
streams.forEach(stream => {
stream.url = proxyBaseUrl + slug + stream.url + ref;
stream.originalUrl = proxyBaseUrl + slug + stream.originalUrl + ref;
});
return streams
}
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url, audio, providerId, hdr = {}) {
audio = audio.toUpperCase()
var streams = [{
url: url,
originalUrl: url,
quality: `Auto - ${providerId} : ${audio}`,
headers: hdr
}];
var doExtract = this.getPreference("aniplay_pref_extract_streams");
// Pahe only has auto
if (providerId === "pahe" || !doExtract) {
return streams;
}
const response = await this.client.get(url, hdr);
const body = response.body;
const lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
var m3u8Url = lines[i + 1].trim();
if (providerId === "yuki") {
var orginalUrl = url
m3u8Url = orginalUrl.replace("master.m3u8", m3u8Url)
}
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: `${resolution} - ${providerId} : ${audio}`,
headers: hdr
});
}
}
return streams
}
async getYukiStreams(result, audio) {
var m3u8Url = result.sources[0].url
var streams = await this.extractStreams(m3u8Url, audio, "yuki");
var subtitles = []
result.subtitles.forEach(sub => {
var label = sub.label
if (label.indexOf("thumbnail") < 0) { // thumbnails shouldnt be included
subtitles.push({
"label": sub.lang,
"file": sub.url,
});
}
})
streams[0].subtitles = subtitles
return streams
}
async getPaheStreams(result, audio) {
var m3u8Url = result.sources[0].url
var hdr = result.headers;
return await this.extractStreams(m3u8Url, audio, "pahe", hdr);
}
//------------ Extract keys ---------------
async extractKeys(baseUrl) { async extractKeys(baseUrl) {
const preferences = new SharedPreferences(); const preferences = new SharedPreferences();
let KEYS = preferences.getString("aniplay_keys", ""); let KEYS = preferences.getString("aniplay_keys", "");