Merge pull request #252 from Swakshan/anime/netmirror

Rework: Netmirror
This commit is contained in:
Moustapha Kodjo Amadou
2025-04-27 15:23:30 +01:00
committed by GitHub

View File

@@ -2,26 +2,27 @@ const mangayomiSources = [{
"name": "NetMirror", "name": "NetMirror",
"id": 446414301, "id": 446414301,
"lang": "all", "lang": "all",
"baseUrl": "https://iosmirror.cc", "baseUrl": "https://netfree2.cc",
"apiUrl": "https://pcmirror.cc", "apiUrl": "https://netfree2.cc",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png", "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png",
"typeSource": "single", "typeSource": "single",
"itemType": 1, "itemType": 1,
"version": "0.2.1", "version": "0.3.4",
"pkgPath": "anime/src/all/netflixmirror.js" "pkgPath": "anime/src/all/netflixmirror.js"
}]; }];
class DefaultExtension extends MProvider { class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getPreference(key) { getPreference(key) {
const preferences = new SharedPreferences(); const preferences = new SharedPreferences();
return preferences.get(key); return preferences.get(key);
} }
getMobileBaseUrl() {
return this.getPreference("netmirror_override_mobile_base_url");
}
getTVBaseUrl() { getTVBaseUrl() {
return this.getPreference("netmirror_override_tv_base_url"); return this.getPreference("netmirror_override_tv_base_url");
} }
@@ -37,8 +38,7 @@ class DefaultExtension extends MProvider {
return `https://imgcdn.media/pv/480/${id}.jpg` return `https://imgcdn.media/pv/480/${id}.jpg`
} }
async getCookie(service) {
async getCookie() {
const preferences = new SharedPreferences(); const preferences = new SharedPreferences();
let cookie = preferences.getString("cookie", ""); let cookie = preferences.getString("cookie", "");
var cookie_ts = parseInt(preferences.getString("cookie_ts", "0")); var cookie_ts = parseInt(preferences.getString("cookie_ts", "0"));
@@ -46,64 +46,81 @@ class DefaultExtension extends MProvider {
// Cookie lasts for 24hrs but still checking for 12hrs // Cookie lasts for 24hrs but still checking for 12hrs
if (now_ts - cookie_ts > 60 * 60 * 12) { if (now_ts - cookie_ts > 60 * 60 * 12) {
const check = await new Client().get(this.getMobileBaseUrl() + `/mobile/home`, { "cookie": cookie }); var baseUrl = this.getTVBaseUrl()
const check = await this.client.get(baseUrl + `/mobile/home`, { "cookie": cookie });
const hDocBody = new Document(check.body).selectFirst("body") const hDocBody = new Document(check.body).selectFirst("body")
const addhash = hDocBody.attr("data-addhash"); const addhash = hDocBody.attr("data-addhash");
const data_time = hDocBody.attr("data-time"); const data_time = hDocBody.attr("data-time");
var res = await new Client().post(`${this.getTVBaseUrl()}/tv/p.php`, { "cookie": "" }, { "hash": addhash }); var res = await this.client.post(`${baseUrl}/tv/p.php`, { "cookie": "" }, { "hash": addhash });
cookie = res.headers["set-cookie"]; cookie = res.headers["set-cookie"];
preferences.setString("cookie", cookie); preferences.setString("cookie", cookie);
preferences.setString("cookie_ts", data_time); preferences.setString("cookie_ts", data_time);
} }
var service = this.getServiceDetails(); service = service ?? this.getServiceDetails();
return `ott=${service}; ${cookie}`; return `ott=${service}; ${cookie}`;
} }
async request(url, cookie) {
cookie = cookie ?? await this.getCookie();
async request(slug, service = null, cookie = null) {
var service = service ?? this.getServiceDetails();
var cookie = cookie ?? await this.getCookie();
var srv = ""
if (service === "pv") srv = "/" + service
var url = this.getTVBaseUrl() + "/tv" + srv + slug
return (await this.client.get(url, { "cookie": cookie })).body;
}
async getHome(body) {
var service = this.getServiceDetails(); var service = this.getServiceDetails();
var slug = ""; var list = []
if (url == "/home") slug = ""; if (service === "nf") {
else if (service == "pv") slug = "/pv"; var body = await this.request("/home", service)
if (!(url.startsWith("https"))) { var elements = new Document(body).select("a.slider-item.boxart-container.open-modal.focusme");
url = this.getMobileBaseUrl() + "/mobile" + slug + url
}
return (await new Client().get(url, { "cookie": cookie })).body; elements.forEach(item => {
} var id = item.attr("data-post")
async getPopular(page) {
return await this.getPages(await this.request("/home"), ".tray-container, #top10")
}
async getLatestUpdates(page) {
return await this.getPages(await this.request("/home"), ".inner-mob-tray-container")
}
async getPages(body, selector) {
var name_pref = this.getPreference("netmirror_pref_display_name_1");
const elements = new Document(body).select(selector);
const cookie = await this.getCookie();
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("article, .top10-post");
const id = linkElement.selectFirst("a").attr("data-post");
if (id.length > 0) { if (id.length > 0) {
const imageUrl = linkElement.selectFirst(".card-img-container img, .top10-img img").attr("data-src"); var imageUrl = this.getPoster(id, service)
var name = name_pref ? JSON.parse(await this.request(`/post.php?id=${id}`, cookie)).title : `\n${id}` // Having no name breaks the script so having "id" as name
var name = `\n${id}`
list.push({ name, imageUrl, link: id }); list.push({ name, imageUrl, link: id })
} }
})
} else {
var body = await this.request("/homepage.php", service)
var elements = JSON.parse(body).post
elements.forEach(item => {
var ids = item.ids
ids.split(",").forEach(id => {
var imageUrl = this.getPoster(id, service)
// Having no name breaks the script so having "id" as name
var name = `\n${id}`
list.push({ name, imageUrl, link: id })
})
})
} }
return { return {
list: list, list: list,
hasNextPage: false hasNextPage: false
} }
} }
async getPopular(page) {
return await this.getHome()
}
async getLatestUpdates(page) {
return await this.getHome()
}
async search(query, page, filters) { async search(query, page, filters) {
var service = this.getServiceDetails(); var service = this.getServiceDetails();
const data = JSON.parse(await this.request(`/search.php?s=${query}`)); const data = JSON.parse(await this.request(`/search.php?s=${query}`, service));
const list = []; const list = [];
data.searchResult.map(async (res) => { data.searchResult.map(async (res) => {
const id = res.id; const id = res.id;
@@ -115,57 +132,62 @@ class DefaultExtension extends MProvider {
hasNextPage: false hasNextPage: false
} }
} }
async getDetail(url) { async getDetail(url) {
var service = this.getServiceDetails(); var service = this.getServiceDetails();
const cookie = await this.getCookie(); var cookie = await this.getCookie(service);
var linkSlug = "https://netflix.com/title/"
if (service === "pv") linkSlug = `https://www.primevideo.com/detail/`
const data = JSON.parse(await this.request(`/post.php?id=${url}`, cookie)); // Check needed while refreshing existing data
var vidId = url
if (url.includes(linkSlug)) vidId = url.replaceAll(linkSlug, '')
const data = JSON.parse(await this.request(`/post.php?id=${vidId}`));
const name = data.title; const name = data.title;
const genre = [data.ua, ...(data.genre || '').split(',').map(g => g.trim())]; const genre = [data.ua, ...(data.genre || '').split(',').map(g => g.trim())];
const description = data.desc; const description = data.desc;
let episodes = []; let episodes = [];
if (data.episodes[0] === null) {
episodes.push({ name, url: url }); var seasons = data.season
} else { if (seasons) {
episodes = data.episodes.map(ep => ({
name: `${ep.s.replace('S', 'Season ')} ${ep.ep.replace('E', 'Episode ')} : ${ep.t}`,
url: ep.id
}));
}
if (data.nextPageShow === 1) {
const eps = await this.getEpisodes(name, url, data.nextPageSeason, 2, cookie);
episodes.push(...eps);
}
episodes.reverse();
if (data.season && data.season.length > 1) {
let newEpisodes = []; let newEpisodes = [];
const seasonsToProcess = data.season.slice(0, -1); await Promise.all(seasons.map(async (season) => {
await Promise.all(seasonsToProcess.map(async (season) => { const eps = await this.getEpisodes(name, vidId, season.id, 1, service, cookie);
const eps = await this.getEpisodes(name, url, season.id, 1, cookie);
newEpisodes.push(...eps); newEpisodes.push(...eps);
})); }));
newEpisodes.reverse();
episodes.push(...newEpisodes); episodes.push(...newEpisodes);
} else {
// For movies aka if there are no seasons and episodes
episodes.push({
name: `Movie`,
url: vidId
});
} }
var service = this.getServiceDetails(); var link = `${linkSlug}${vidId}`
var link = `https://netflix.com/title/${url}`
if (service === "pv") link = `https://www.primevideo.com/detail/${url}`
return { return {
name, imageUrl: this.getPoster(url, service), link, description, status: 1, genre, episodes name, imageUrl: this.getPoster(vidId, service), link, description, status: 1, genre, episodes
}; };
} }
async getEpisodes(name, eid, sid, page, cookie) {
async getEpisodes(name, eid, sid, page, service, cookie) {
const episodes = []; const episodes = [];
let pg = page; let pg = page;
while (true) { while (true) {
try { try {
const data = JSON.parse(await this.request(`/episodes.php?s=${sid}&series=${eid}&page=${pg}`, cookie)); const data = JSON.parse(await this.request(`/episodes.php?s=${sid}&series=${eid}&page=${pg}`, service, cookie));
data.episodes?.forEach(ep => { data.episodes?.forEach(ep => {
var season = ep.s.replace('S', 'Season ')
var epNum = ep.ep.replace("E", "")
var epText = `Episode ${epNum}`
var title = ep.t
title = title == epText ? title : `${epText}: ${title}`
episodes.push({ episodes.push({
name: `${ep.s.replace('S', 'Season ')} ${ep.ep.replace('E', 'Episode ')} : ${ep.t}`, name: `${season} ${title}`,
url: ep.id url: ep.id
}); });
}); });
@@ -177,7 +199,7 @@ class DefaultExtension extends MProvider {
} }
} }
return episodes; return episodes.reverse();
} }
// Sorts streams based on user preference. // Sorts streams based on user preference.
@@ -201,27 +223,18 @@ class DefaultExtension extends MProvider {
} }
async getVideoList(url) { async getVideoList(url) {
var slug = ""
var src = this.getPreference("netmirror_pref_stream_extraction");
var service = this.getServiceDetails();
// prime extracton works only in mobile var baseUrl = this.getTVBaseUrl()
if (service == "pv") { var url = `/playlist.php?id=${url}`
slug = "/pv"
src = "mobile"
}
var device = "/mobile"
if (src == 'tv') device = "/tv";
var baseUrl = src === 'tv' ? this.getTVBaseUrl() : this.getMobileBaseUrl()
url = baseUrl + device + slug + `/playlist.php?id=${url}`
const data = JSON.parse(await this.request(url)); const data = JSON.parse(await this.request(url));
let videoList = []; let videoList = [];
let subtitles = []; let subtitles = [];
let audios = []; let audios = [];
for (const playlist of data) { var playlist = data[0]
var source = playlist.sources[0] var source = playlist.sources[0]
var link = baseUrl + source.file; var link = baseUrl + source.file;
var headers = var headers =
{ {
@@ -229,7 +242,10 @@ class DefaultExtension extends MProvider {
'Referer': `${baseUrl}/` 'Referer': `${baseUrl}/`
}; };
var resp = await new Client().get(link, headers); // Auto
videoList.push({ url: link, quality: "Auto", "originalUrl": link, headers });
var resp = await this.client.get(link, headers);
if (resp.statusCode === 200) { if (resp.statusCode === 200) {
const masterPlaylist = resp.body; const masterPlaylist = resp.body;
@@ -247,18 +263,13 @@ class DefaultExtension extends MProvider {
masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => { masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => {
var quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p (${source.label})`; var quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p`;
let videoUrl = it.substringAfter('\n').substringBefore('\n'); let videoUrl = it.substringAfter('\n').substringBefore('\n');
if (!videoUrl.startsWith('http')) { if (!videoUrl.startsWith('http')) {
videoUrl = resp.request.url.substringBeforeLast('/') + `/${videoUrl}`; videoUrl = resp.request.url.substringBeforeLast('/') + `/${videoUrl}`;
} }
var headers = headers['Host'] = videoUrl.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1]
{
'Host': videoUrl.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1],
'Origin': baseUrl,
'Referer': `${baseUrl}/`
};
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, headers }); videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, headers });
}); });
@@ -266,61 +277,37 @@ class DefaultExtension extends MProvider {
if ("tracks" in playlist) { if ("tracks" in playlist) {
playlist.tracks.filter(track => track.kind === 'captions').forEach(track => { await Promise.all(playlist.tracks.map(async (track) => {
if (track.kind == 'captions') {
var subUrl = track.file var subUrl = track.file
subUrl = subUrl.startsWith("//") ? `https:${subUrl}` : subUrl; subUrl = subUrl.startsWith("//") ? `https:${subUrl}` : subUrl;
var subText = await this.client.get(subUrl)
subtitles.push({ subtitles.push({
label: track.label, label: track.label,
file: subUrl file: subText.body
});
}); });
} }
}));
} }
} }
videoList[0].audios = audios; videoList[0].audios = audios;
videoList[0].subtitles = subtitles; videoList[0].subtitles = subtitles;
return this.sortStreams(videoList); return this.sortStreams(videoList);
} }
getSourcePreferences() { getSourcePreferences() {
return [ return [{
{
key: "netmirror_override_mobile_base_url",
editTextPreference: {
title: "Override mobile base url",
summary: "",
value: "https://netfree.cc",
dialogTitle: "Override base url",
dialogMessage: "",
}
}, {
key: "netmirror_override_tv_base_url", key: "netmirror_override_tv_base_url",
editTextPreference: { editTextPreference: {
title: "Override tv base url", title: "Override tv base url",
summary: "", summary: "",
value: "https://pcmirror.cc", value: "https://netfree2.cc",
dialogTitle: "Override base url", dialogTitle: "Override base url",
dialogMessage: "", dialogMessage: "",
} }
}, {
key: 'netmirror_pref_video_resolution',
listPreference: {
title: 'Preferred video resolution',
summary: '',
valueIndex: 0,
entries: ["1080p", "720p", "480p"],
entryValues: ["1080", "720", "480"]
}
}, {
"key": "netmirror_pref_display_name_1",
"switchPreferenceCompat": {
"title": "Display media name on home page",
"summary": "Homepage loads faster by not calling details API",
"value": true
}
}, { }, {
key: 'netmirror_pref_service', key: 'netmirror_pref_service',
listPreference: { listPreference: {
@@ -331,15 +318,15 @@ class DefaultExtension extends MProvider {
entryValues: ["nf", "pv",] entryValues: ["nf", "pv",]
} }
}, { }, {
key: 'netmirror_pref_stream_extraction', key: 'netmirror_pref_video_resolution',
listPreference: { listPreference: {
title: 'Preferred stream extraction source', title: 'Preferred video resolution',
summary: 'Extract stream from which source (if one source fails choose another)', summary: '',
valueIndex: 0, valueIndex: 0,
entries: ["TV", "Mobile"], entries: ["1080p", "720p", "480p"],
entryValues: ["tv", "mobile"] entryValues: ["1080", "720", "480"]
}
} }
},
]; ];
} }