forked from Ninjalama/streamyfin_mirror
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
import * as FileSystem from "expo-file-system";
|
|
import { parseBootXML } from "./parse/boot";
|
|
import { parseStreamInfoXml, StreamInfo } from "./parse/streamInfoBoot";
|
|
|
|
export async function rewriteM3U8Files(baseDir: string): Promise<void> {
|
|
const bootData = await loadBootData(baseDir);
|
|
if (!bootData) return;
|
|
|
|
const localPlaylistPaths = await processAllStreams(baseDir, bootData);
|
|
await updateMasterPlaylist(
|
|
`${baseDir}/Data/${bootData.DataItems.DataItem.DataPath}`,
|
|
localPlaylistPaths
|
|
);
|
|
}
|
|
|
|
async function loadBootData(baseDir: string): Promise<any | null> {
|
|
const bootPath = `${baseDir}/boot.xml`;
|
|
try {
|
|
const bootInfo = await FileSystem.getInfoAsync(bootPath);
|
|
if (!bootInfo.exists) throw new Error("boot.xml not found");
|
|
|
|
const bootXML = await FileSystem.readAsStringAsync(bootPath);
|
|
return parseBootXML(bootXML);
|
|
} catch (error) {
|
|
console.error(`Failed to load boot.xml from ${baseDir}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function processAllStreams(
|
|
baseDir: string,
|
|
bootData: any
|
|
): Promise<string[]> {
|
|
const localPaths: string[] = [];
|
|
|
|
for (const stream of bootData.Streams.Stream) {
|
|
const streamDir = `${baseDir}/${stream.ID}`;
|
|
try {
|
|
const streamInfo = await processStream(streamDir);
|
|
if (streamInfo && streamInfo.MediaPlaylist.PathToLocalCopy) {
|
|
localPaths.push(
|
|
`${streamDir}/${streamInfo.MediaPlaylist.PathToLocalCopy}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Skipping stream ${stream.ID} due to error:`, error);
|
|
}
|
|
}
|
|
return localPaths;
|
|
}
|
|
|
|
async function updateMasterPlaylist(
|
|
masterPath: string,
|
|
localPlaylistPaths: string[]
|
|
): Promise<void> {
|
|
try {
|
|
const masterContent = await FileSystem.readAsStringAsync(masterPath);
|
|
const updatedContent = updatePlaylistWithLocalSegments(
|
|
masterContent,
|
|
localPlaylistPaths
|
|
);
|
|
await FileSystem.writeAsStringAsync(masterPath, updatedContent);
|
|
} catch (error) {
|
|
console.error(`Error updating master playlist at ${masterPath}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function updatePlaylistWithLocalSegments(
|
|
content: string,
|
|
localPaths: string[]
|
|
): string {
|
|
const lines = content.split("\n");
|
|
let index = 0;
|
|
|
|
for (let i = 0; i < lines.length && index < localPaths.length; i++) {
|
|
if (lines[i].trim() && !lines[i].startsWith("#")) {
|
|
lines[i] = localPaths[index++];
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export async function processStream(
|
|
streamDir: string
|
|
): Promise<StreamInfo | null> {
|
|
const streamInfoPath = `${streamDir}/StreamInfoBoot.xml`;
|
|
|
|
try {
|
|
const streamXML = await FileSystem.readAsStringAsync(streamInfoPath);
|
|
const streamInfo = await parseStreamInfoXml(streamXML);
|
|
|
|
const localM3u8RelPath = streamInfo.MediaPlaylist?.PathToLocalCopy;
|
|
if (!localM3u8RelPath) {
|
|
console.warn(`No local m3u8 specified in ${streamDir}; skipping.`);
|
|
return null;
|
|
}
|
|
|
|
const m3u8Path = `${streamDir}/${localM3u8RelPath}`;
|
|
const m3u8Content = await FileSystem.readAsStringAsync(m3u8Path);
|
|
|
|
const localSegmentPaths = streamInfo.MediaSegments.SEG.map(
|
|
(seg) => `${streamDir}/${seg.PATH}`
|
|
);
|
|
const updatedContent = updatePlaylistWithLocalSegments(
|
|
m3u8Content,
|
|
localSegmentPaths
|
|
);
|
|
await FileSystem.writeAsStringAsync(m3u8Path, updatedContent);
|
|
|
|
return streamInfo;
|
|
} catch (error) {
|
|
console.error(`Error processing stream at ${streamDir}:`, error);
|
|
throw error;
|
|
}
|
|
}
|