mirror of
https://github.com/BuddyChewChew/My-Streams.git
synced 2025-09-09 10:52:08 +02:00
Copy the code in the file and redeploy. Service addresses will change. Have to update your player.
417 lines
No EOL
18 KiB
JavaScript
417 lines
No EOL
18 KiB
JavaScript
function doGet(e) {
|
|
const params = e.parameter;
|
|
const region = (params.region || 'us').toLowerCase().trim();
|
|
const service = params.service;
|
|
const sort = params.sort || 'name';
|
|
if (!service) {
|
|
return handleError('Error: No service type provided');
|
|
}
|
|
// Handle Pluto TV service
|
|
if (service.toLowerCase() === 'plutotv') {
|
|
return handlePluto(region, sort);
|
|
}
|
|
// Handle Plex service with updated User-Agent
|
|
if (service.toLowerCase() === 'plex') {
|
|
return handlePlex(region, sort);
|
|
}
|
|
// Handle SamsungTVPlus service
|
|
if (service.toLowerCase() === 'samsungtvplus') {
|
|
return handleSamsungTVPlus(region, sort);
|
|
}
|
|
// Handle Roku service
|
|
if (service.toLowerCase() === 'roku') {
|
|
return handleRoku(sort);
|
|
}
|
|
// Handle Stirr service
|
|
if (service.toLowerCase() === 'stirr') {
|
|
return handleStirr(sort);
|
|
}
|
|
// Handle Tubi service with new URL
|
|
if (service.toLowerCase() === 'tubi') {
|
|
return handleTubi(service);
|
|
}
|
|
// Handle PBSKids service
|
|
if (service.toLowerCase() === 'pbskids') {
|
|
return handlePBSKids(service);
|
|
}
|
|
// Handle PBS service
|
|
if (service.toLowerCase() === 'pbs') {
|
|
return handlePBS();
|
|
}
|
|
// If no matching service was found, return an error
|
|
return handleError('Error: Unsupported service type provided');
|
|
}
|
|
|
|
//------ Service Functions ------//
|
|
|
|
function handlePluto(region, sort) {
|
|
const PLUTO_URL = 'https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/PlutoTV/.channels.json.gz';
|
|
const STREAM_URL_TEMPLATE = 'https://jmp2.uk/plu-{id}.m3u8';
|
|
sort = sort || 'name';
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new Pluto data from URL: ' + PLUTO_URL);
|
|
const response = UrlFetchApp.fetch(PLUTO_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
Logger.log('Data successfully extracted and parsed.');
|
|
} catch (error) {
|
|
Logger.log('Error fetching or processing Pluto data: ' + error.stack);
|
|
return handleError('Error fetching Pluto data: ' + error.message);
|
|
}
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/master/PlutoTV/${region}.xml.gz"\n`;
|
|
const regionNameMap = {
|
|
ar: "Argentina", br: "Brazil", ca: "Canada", cl: "Chile", de: "Germany",
|
|
dk: "Denmark", es: "Spain", fr: "France", gb: "United Kingdom", it: "Italy",
|
|
mx: "Mexico", no: "Norway", se: "Sweden", us: "United States"
|
|
};
|
|
let channels = {};
|
|
if (region === 'all') {
|
|
for (const regionKey in data.regions) {
|
|
const regionData = data.regions[regionKey];
|
|
const regionFullName = regionNameMap[regionKey] || regionKey.toUpperCase();
|
|
for (const channelKey in regionData.channels) {
|
|
const channel = { ...regionData.channels[channelKey], region: regionFullName };
|
|
const uniqueChannelId = `${channelKey}-${regionKey}`;
|
|
channels[uniqueChannelId] = channel;
|
|
}
|
|
}
|
|
} else {
|
|
if (!data.regions[region]) {
|
|
return handleError(`Error: Region '${region}' not found in Pluto data.`);
|
|
}
|
|
channels = data.regions[region].channels || {};
|
|
}
|
|
const sortedChannelIds = Object.keys(channels).sort((a, b) => {
|
|
const channelA = channels[a];
|
|
const channelB = channels[b];
|
|
if (sort === 'chno') {
|
|
return channelA.chno - channelB.chno;
|
|
} else {
|
|
return channelA.name.localeCompare(channelB.name);
|
|
}
|
|
});
|
|
sortedChannelIds.forEach(channelId => {
|
|
const channel = channels[channelId];
|
|
const { chno, name, logo, group, region: channelRegion } = channel;
|
|
const groupTitle = region === 'all' ? `${channelRegion}` : group;
|
|
output += `#EXTINF:-1 channel-id="${channelId}" tvg-id="${channelId}" tvg-chno="${chno}" tvg-name="${name}" tvg-logo="${logo}" group-title="${groupTitle}", ${name}\n`;
|
|
output += STREAM_URL_TEMPLATE.replace('{id}', channelId.split('-')[0]) + '\n';
|
|
});
|
|
output = output.replace(/tvg-id="(.*?)-\w{2}"/g, 'tvg-id="$1"');
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
function handlePlex(region, sort) {
|
|
const PLEX_URL = 'https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/Plex/.channels.json.gz';
|
|
const CHANNELS_JSON_URL = 'https://raw.githubusercontent.com/Mikoshi-nyudo/plex-channels-list/refs/heads/main/plex/channels.json';
|
|
const STREAM_URL_TEMPLATE = 'https://jmp2.uk/plex-{id}.m3u8';
|
|
const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36';
|
|
sort = sort || 'name';
|
|
let data;
|
|
let plexChannels = [];
|
|
try {
|
|
Logger.log('Fetching new Plex data from URL: ' + PLEX_URL);
|
|
const options = {
|
|
'headers': { 'User-Agent': USER_AGENT }
|
|
};
|
|
const response = UrlFetchApp.fetch(PLEX_URL, options);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
Logger.log('Fetching new channels.json data from URL: ' + CHANNELS_JSON_URL);
|
|
const channelsResponse = UrlFetchApp.fetch(CHANNELS_JSON_URL, options);
|
|
plexChannels = JSON.parse(channelsResponse.getContentText());
|
|
} catch (error) {
|
|
Logger.log('Error fetching Plex or channels data: ' + error.stack);
|
|
return handleError('Error fetching Plex or channels data: ' + error.message);
|
|
}
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/master/Plex/${region}.xml.gz"\n`;
|
|
const regionNameMap = {
|
|
us: "United States", mx: "Mexico", es: "Spain", ca: "Canada", au: "Australia", nz: "New Zealand"
|
|
};
|
|
let channels = {};
|
|
if (region === 'all') {
|
|
for (const regionKey in data.regions) {
|
|
const regionData = data.regions[regionKey];
|
|
const regionFullName = regionNameMap[regionKey] || regionKey.toUpperCase();
|
|
for (const channelKey in data.channels) {
|
|
const channel = data.channels[channelKey];
|
|
if (channel.regions.includes(regionKey)) {
|
|
const uniqueChannelId = `${channelKey}-${regionKey}`;
|
|
channels[uniqueChannelId] = { ...channel, region: regionFullName, group: regionFullName, originalId: channelKey };
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (!data.regions[region]) {
|
|
return handleError(`Error: Region '${region}' not found in Plex data.`);
|
|
}
|
|
for (const channelKey in data.channels) {
|
|
const channel = data.channels[channelKey];
|
|
if (channel.regions.includes(region)) {
|
|
const matchingChannel = plexChannels.find(ch => ch.Title === channel.name);
|
|
const genre = matchingChannel && matchingChannel.Genre ? matchingChannel.Genre : 'Uncategorized';
|
|
channels[channelKey] = { ...channel, group: genre, originalId: channelKey };
|
|
}
|
|
}
|
|
}
|
|
const sortedChannelIds = Object.keys(channels).sort((a, b) => {
|
|
const channelA = channels[a];
|
|
const channelB = channels[b];
|
|
return sort === 'chno' ? (channelA.chno - channelB.chno) : channelA.name.localeCompare(channelB.name);
|
|
});
|
|
sortedChannelIds.forEach(channelId => {
|
|
const channel = channels[channelId];
|
|
const { chno, name, logo, group, originalId } = channel;
|
|
output += `#EXTINF:-1 channel-id="${channelId}" tvg-id="${channelId}" tvg-chno="${chno || ''}" tvg-name="${name}" tvg-logo="${logo}" group-title="${group}", ${name}\n`;
|
|
output += STREAM_URL_TEMPLATE.replace('{id}', originalId) + '\n';
|
|
});
|
|
output = output.replace(/tvg-id="(.*?)-\w{2}"/g, 'tvg-id="$1"');
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
function handleSamsungTVPlus(region, sort) {
|
|
const SAMSUNG_URL = 'https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/SamsungTVPlus/.channels.json.gz';
|
|
const STREAM_URL_TEMPLATE = 'https://jmp2.uk/{slug}';
|
|
sort = sort || 'name';
|
|
let data;
|
|
|
|
try {
|
|
Logger.log('Fetching new SamsungTVPlus data from URL: ' + SAMSUNG_URL);
|
|
const response = UrlFetchApp.fetch(SAMSUNG_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
} catch (error) {
|
|
Logger.log('Error fetching or processing SamsungTVPlus data: ' + error.stack);
|
|
return handleError('Error fetching SamsungTVPlus data: ' + error.message);
|
|
}
|
|
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/master/SamsungTVPlus/${region}.xml.gz"\n`;
|
|
let channels = {};
|
|
|
|
if (region === 'all') {
|
|
for (const regionKey in data.regions) {
|
|
const regionData = data.regions[regionKey];
|
|
const regionFullName = regionData.name || regionKey.toUpperCase();
|
|
for (const channelKey in regionData.channels) {
|
|
const channel = {
|
|
...regionData.channels[channelKey],
|
|
region: regionFullName,
|
|
regionKey: regionKey,
|
|
uniqueId: `${channelKey}-${regionKey}`
|
|
};
|
|
channels[channel.uniqueId] = channel;
|
|
}
|
|
}
|
|
} else {
|
|
if (!data.regions[region]) {
|
|
return handleError(`Error: Region '${region}' not found in SamsungTVPlus data.`);
|
|
}
|
|
for (const channelKey in data.regions[region].channels) {
|
|
const channel = {
|
|
...data.regions[region].channels[channelKey],
|
|
uniqueId: channelKey
|
|
};
|
|
channels[channelKey] = channel;
|
|
}
|
|
}
|
|
|
|
const sortedChannelIds = Object.keys(channels).sort((a, b) => {
|
|
const channelA = channels[a];
|
|
const channelB = channels[b];
|
|
if (sort === 'chno') {
|
|
return channelA.chno - channelB.chno;
|
|
} else {
|
|
return channelA.name.localeCompare(channelB.name);
|
|
}
|
|
});
|
|
|
|
sortedChannelIds.forEach(channelId => {
|
|
const channel = channels[channelId];
|
|
const { chno, name, logo, group, region: channelRegion, slug } = channel;
|
|
const groupTitle = region === 'all' ? `${channelRegion}` : group;
|
|
|
|
// Use the channel's slug or fall back to 'stvp-{id}' if not provided
|
|
const channelSlug = slug ?
|
|
slug.replace('{id}', channel.uniqueId.split('-')[0]) :
|
|
`stvp-${channel.uniqueId.split('-')[0]}`;
|
|
const streamUrl = STREAM_URL_TEMPLATE.replace('{slug}', channelSlug);
|
|
|
|
output += `#EXTINF:-1 channel-id="${channelId}" tvg-id="${channelId}" tvg-chno="${chno}" tvg-name="${name}" tvg-logo="${logo}" group-title="${groupTitle}", ${name}\n`;
|
|
output += streamUrl + '\n';
|
|
});
|
|
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
function handleRoku(sort) {
|
|
const ROKU_URL = 'https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/Roku/.channels.json.gz';
|
|
const STREAM_URL_TEMPLATE = 'https://jmp2.uk/rok-{id}.m3u8';
|
|
sort = sort || 'name';
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new Roku data from URL: ' + ROKU_URL);
|
|
const response = UrlFetchApp.fetch(ROKU_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
} catch (error) {
|
|
Logger.log('Error fetching or processing Roku data: ' + error.stack);
|
|
return handleError('Error fetching Roku data: ' + error.message);
|
|
}
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/master/Roku/all.xml.gz"\n`;
|
|
let channels = data.channels || {};
|
|
const sortedChannelIds = Object.keys(channels).sort((a, b) => {
|
|
const channelA = channels[a];
|
|
const channelB = channels[b];
|
|
if (sort === 'chno') {
|
|
return channelA.chno - channelB.chno;
|
|
} else {
|
|
return channelA.name.localeCompare(channelB.name);
|
|
}
|
|
});
|
|
sortedChannelIds.forEach(channelId => {
|
|
const channel = channels[channelId];
|
|
const { chno, name, logo, groups } = channel;
|
|
const groupTitle = groups && groups.length > 0 ? groups[0] : 'Uncategorized';
|
|
output += `#EXTINF:-1 channel-id="${channelId}" tvg-id="${channelId}" tvg-chno="${chno}" tvg-name="${name}" tvg-logo="${logo}" group-title="${groupTitle}", ${name}\n`;
|
|
output += STREAM_URL_TEMPLATE.replace('{id}', channelId) + '\n';
|
|
});
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
function handleStirr(sort) {
|
|
const STIRR_URL = 'https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/Stirr/.channels.json.gz';
|
|
sort = sort || 'name';
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new Stirr data from URL: ' + STIRR_URL);
|
|
const response = UrlFetchApp.fetch(STIRR_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
} catch (error) {
|
|
Logger.log('Error fetching or processing Stirr data: ' + error.stack);
|
|
return handleError('Error fetching Stirr data: ' + error.message);
|
|
}
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/refs/heads/master/Stirr/all.xml.gz"\n`;
|
|
let channels = data.channels || {};
|
|
const sortedChannelIds = Object.keys(channels).sort((a, b) => {
|
|
const channelA = channels[a];
|
|
const channelB = channels[b];
|
|
if (sort === 'chno') {
|
|
return channelA.chno - channelB.chno;
|
|
} else {
|
|
return channelA.name.localeCompare(channelB.name);
|
|
}
|
|
});
|
|
sortedChannelIds.forEach(channelId => {
|
|
const channel = channels[channelId];
|
|
const { chno, name, logo, groups } = channel;
|
|
const groupTitle = groups && groups.length > 0 ? groups.join(', ') : 'Uncategorized';
|
|
const streamUrl = `https://jmp2.uk/str-${channelId}.m3u8`;
|
|
output += `#EXTINF:-1 channel-id="${channelId}" tvg-id="${channelId}" tvg-chno="${chno}" tvg-name="${name}" tvg-logo="${logo}" group-title="${groupTitle}", ${name}\n`;
|
|
output += `${streamUrl}\n`;
|
|
});
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
function handleTubi(service) {
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new Tubi data');
|
|
const playlistUrl = 'https://raw.githubusercontent.com/BuddyChewChew/tubi-scraper/refs/heads/main/tubi_playlist.m3u';
|
|
const options = {
|
|
'headers': {
|
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
|
|
}
|
|
};
|
|
const response = UrlFetchApp.fetch(playlistUrl, options);
|
|
data = response.getContentText();
|
|
let epgUrl = 'https://raw.githubusercontent.com/BuddyChewChew/tubi-scraper/refs/heads/main/tubi_epg.xml';
|
|
let output = `#EXTM3U url-tvg="${epgUrl}"\n`;
|
|
output += data;
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
} catch (error) {
|
|
Logger.log('Error fetching Tubi data: ' + error.stack);
|
|
return handleError('Error fetching Tubi data: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function handlePBSKids(service) {
|
|
if (service.toLowerCase() !== 'pbskids') return;
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new PBS Kids data');
|
|
const APP_URL = 'https://i.mjh.nz/PBS/.kids_app.json.gz';
|
|
const response = UrlFetchApp.fetch(APP_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
let output = `#EXTM3U url-tvg="https://github.com/matthuisman/i.mjh.nz/raw/master/PBS/kids_all.xml.gz"\n`;
|
|
const sortedKeys = Object.keys(data.channels).sort((a, b) => {
|
|
const channelA = data.channels[a].name.toLowerCase();
|
|
const channelB = data.channels[b].name.toLowerCase();
|
|
return channelA.localeCompare(channelB);
|
|
});
|
|
sortedKeys.forEach(key => {
|
|
const channel = data.channels[key];
|
|
const { logo, name, url } = channel;
|
|
output += `#EXTINF:-1 channel-id="pbskids-${key}" tvg-id="${key}" tvg-name="${name}" tvg-logo="${logo}", ${name}\n${url}\n`;
|
|
});
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
} catch (error) {
|
|
Logger.log('Error fetching PBS Kids data: ' + error.stack);
|
|
return handleError('Error fetching PBS Kids data: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function handlePBS() {
|
|
const DATA_URL = 'https://i.mjh.nz/PBS/.app.json.gz';
|
|
const EPG_URL = 'https://i.mjh.nz/PBS/all.xml.gz';
|
|
let data;
|
|
try {
|
|
Logger.log('Fetching new PBS data from URL: ' + DATA_URL);
|
|
const response = UrlFetchApp.fetch(DATA_URL);
|
|
let gzipBlob = response.getBlob();
|
|
gzipBlob = gzipBlob.setContentType('application/x-gzip');
|
|
const extractedBlob = Utilities.ungzip(gzipBlob);
|
|
const extractedData = extractedBlob.getDataAsString();
|
|
data = JSON.parse(extractedData);
|
|
} catch (error) {
|
|
Logger.log('Error fetching or processing PBS data: ' + error.stack);
|
|
return handleError('Error fetching PBS data: ' + error.message);
|
|
}
|
|
let output = `#EXTM3U x-tvg-url="${EPG_URL}"\n`;
|
|
Object.keys(data.channels).forEach(key => {
|
|
const channel = data.channels[key];
|
|
output += `#EXTINF:-1 channel-id="pbs-${key}" tvg-id="${key}" tvg-name="${channel.name}" tvg-logo="${channel.logo}", ${channel.name}\n`;
|
|
output += `#KODIPROP:inputstream.adaptive.manifest_type=mpd\n`;
|
|
output += `#KODIPROP:inputstream.adaptive.license_type=com.widevine.alpha\n`;
|
|
output += `#KODIPROP:inputstream.adaptive.license_key=${channel.license}|Content-Type=application%2Foctet-stream&user-agent=okhttp%2F4.9.0|R{SSM}|\n`;
|
|
output += `${channel.url}|user-agent=okhttp%2F4.9.0\n`;
|
|
});
|
|
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.TEXT);
|
|
}
|
|
|
|
//------ Other Functions ------//
|
|
|
|
function handleError(errorMessage) {
|
|
return ContentService.createTextOutput(errorMessage)
|
|
.setMimeType(ContentService.MimeType.TEXT);
|
|
} |