mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-11 12:07:54 +00:00
feat(torrent): add magnet and torrent handoff
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ExtensionCookieScope } from "./ExtensionCookieScope";
|
||||
|
||||
export type ExtensionDownload = { request_id?: string, urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, cookie_scopes: Array<ExtensionCookieScope> | null, media: boolean, batch: boolean, batch_name: string | null, };
|
||||
export type ExtensionDownload = { request_id?: string, urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, cookie_scopes: Array<ExtensionCookieScope> | null, media: boolean, torrent: boolean, batch: boolean, batch_name: string | null, };
|
||||
|
||||
@@ -62,10 +62,12 @@ const formatBytes = (bytes: number) => {
|
||||
};
|
||||
|
||||
const normalizeComparableUrl = (rawUrl: string) => {
|
||||
const trimmed = rawUrl.trim();
|
||||
if (trimmed.startsWith('/') || /^[a-z]:[\\/]/i.test(trimmed)) return trimmed;
|
||||
try {
|
||||
return new URL(rawUrl).href;
|
||||
return new URL(trimmed).href;
|
||||
} catch {
|
||||
return rawUrl.trim();
|
||||
return trimmed;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -141,6 +143,7 @@ export const AddDownloadsModal = () => {
|
||||
pendingAddHeaders,
|
||||
pendingAddCookies,
|
||||
pendingAddMediaUrls,
|
||||
pendingAddTorrentUrls,
|
||||
pendingAddBatchName,
|
||||
pendingAddRequestContexts,
|
||||
pendingAddRequestVersion,
|
||||
@@ -530,13 +533,8 @@ export const AddDownloadsModal = () => {
|
||||
return Object.keys(retained).length === Object.keys(current).length ? current : retained;
|
||||
});
|
||||
|
||||
const forcedMediaUrls = new Set(pendingAddMediaUrls.map(url => {
|
||||
try {
|
||||
return new URL(url).href;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}));
|
||||
const forcedMediaUrls = new Set(pendingAddMediaUrls.map(normalizeComparableUrl));
|
||||
const forcedTorrentUrls = new Set(pendingAddTorrentUrls.map(normalizeComparableUrl));
|
||||
const requestFilenames = Object.fromEntries(
|
||||
Object.entries(pendingAddRequestContexts)
|
||||
.filter(([, context]) => Boolean(context.filename))
|
||||
@@ -564,13 +562,15 @@ export const AddDownloadsModal = () => {
|
||||
requestFilenames,
|
||||
requestContextVersions,
|
||||
playlistExpansions,
|
||||
selectedBySourceUrl
|
||||
selectedBySourceUrl,
|
||||
forcedTorrentUrls
|
||||
);
|
||||
});
|
||||
}, [
|
||||
urls,
|
||||
pendingAddFilename,
|
||||
pendingAddMediaUrls,
|
||||
pendingAddTorrentUrls,
|
||||
pendingAddRequestContexts,
|
||||
hasExtensionRequestContext,
|
||||
playlistExpansions
|
||||
@@ -608,7 +608,11 @@ export const AddDownloadsModal = () => {
|
||||
source: row.sourceUrl,
|
||||
id: torrentCacheId,
|
||||
cache: true,
|
||||
proxy: proxy ?? undefined
|
||||
proxy: proxy ?? undefined,
|
||||
headers: headersForRow(contextUrl) || undefined,
|
||||
cookies: cookiesForRow(contextUrl, row.sourceUrl) || undefined,
|
||||
cookieScopes: requestContext?.cookieScopes || undefined,
|
||||
torrent: true
|
||||
});
|
||||
const isCurrentTorrentDraft = addModalOpenRef.current
|
||||
&& parsedItemsRef.current.some(currentRow =>
|
||||
@@ -1465,6 +1469,7 @@ export const AddDownloadsModal = () => {
|
||||
try {
|
||||
const id = crypto.randomUUID();
|
||||
allocatedId = id;
|
||||
const contextUrl = requestContextUrlForRow(item);
|
||||
let torrentPath = item.torrentPath;
|
||||
if (item.isTorrent) {
|
||||
if (item.torrentPath) {
|
||||
@@ -1483,7 +1488,11 @@ export const AddDownloadsModal = () => {
|
||||
source: item.sourceUrl,
|
||||
id,
|
||||
cache: true,
|
||||
proxy: proxy ?? undefined
|
||||
proxy: proxy ?? undefined,
|
||||
headers: headersForRow(contextUrl) || undefined,
|
||||
cookies: cookiesForRow(contextUrl, item.sourceUrl) || undefined,
|
||||
cookieScopes: requestContextForUrl(contextUrl)?.cookieScopes || undefined,
|
||||
torrent: true
|
||||
});
|
||||
torrentPath = torrentData.torrentPath;
|
||||
}
|
||||
@@ -1492,8 +1501,6 @@ export const AddDownloadsModal = () => {
|
||||
? mediaFileNameForSelectedFormat(item.file, item)
|
||||
: canonicalizeDownloadFileName(item.file);
|
||||
let formatSelector = mediaFormatSelectorForRow(item);
|
||||
const contextUrl = requestContextUrlForRow(item);
|
||||
|
||||
const category = categoryForFileName(finalFile);
|
||||
const added = await addDownload({
|
||||
id,
|
||||
|
||||
+10
-1
@@ -41,7 +41,16 @@ type CommandMap = {
|
||||
result: MediaPlaylistMetadata;
|
||||
};
|
||||
inspect_torrent: {
|
||||
args: { source: string; id: string; cache?: boolean; proxy?: string };
|
||||
args: {
|
||||
source: string;
|
||||
id: string;
|
||||
cache?: boolean;
|
||||
proxy?: string;
|
||||
headers?: string;
|
||||
cookies?: string;
|
||||
cookieScopes?: Array<ExtensionCookieScope>;
|
||||
torrent?: boolean;
|
||||
};
|
||||
result: TorrentMetadata;
|
||||
};
|
||||
rekey_torrent_metadata: {
|
||||
|
||||
@@ -2683,6 +2683,7 @@ describe('useDownloadStore', () => {
|
||||
{ url: 'https://accounts.google.com/', cookies: 'SID=account-session' }
|
||||
],
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2721,6 +2722,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: null,
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2744,6 +2746,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'session=secret',
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2768,6 +2771,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: null,
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: true,
|
||||
batch_name: 'Example Gallery'
|
||||
});
|
||||
@@ -2792,6 +2796,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'first=session',
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2804,6 +2809,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'second=session',
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2843,6 +2849,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: `oversized=${'x'.repeat(64 * 1024)}`,
|
||||
cookie_scopes: null,
|
||||
media: true,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2865,6 +2872,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'session=secret',
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2884,6 +2892,7 @@ describe('useDownloadStore', () => {
|
||||
{ url: 'https://media.example/', cookies: 'session=secret' }
|
||||
],
|
||||
media: true,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2903,6 +2912,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'session=secret',
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2915,6 +2925,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: null,
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2945,6 +2956,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: 'session=secret',
|
||||
cookie_scopes: null,
|
||||
media: true,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
@@ -2967,6 +2979,7 @@ describe('useDownloadStore', () => {
|
||||
cookies: null,
|
||||
cookie_scopes: null,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: false,
|
||||
batch_name: null
|
||||
});
|
||||
|
||||
@@ -924,6 +924,7 @@ export type PendingAddRequestContext = {
|
||||
cookies: string;
|
||||
cookieScopes?: ExtensionCookieScope[];
|
||||
media: boolean;
|
||||
torrent?: boolean;
|
||||
};
|
||||
|
||||
export type DeleteModalState = {
|
||||
@@ -956,6 +957,7 @@ interface DownloadState {
|
||||
pendingAddHeaders: string;
|
||||
pendingAddCookies: string;
|
||||
pendingAddMediaUrls: string[];
|
||||
pendingAddTorrentUrls: string[];
|
||||
pendingAddBatch: boolean;
|
||||
pendingAddBatchName: string;
|
||||
pendingAddRequestContexts: Record<string, PendingAddRequestContext>;
|
||||
@@ -971,7 +973,8 @@ interface DownloadState {
|
||||
media?: boolean,
|
||||
cookieScopes?: ExtensionCookieScope[] | null,
|
||||
batch?: boolean,
|
||||
batchName?: string | null
|
||||
batchName?: string | null,
|
||||
torrent?: boolean
|
||||
) => void;
|
||||
handleExtensionDownload: (request: ExtensionDownloadRequest) => Promise<void>;
|
||||
deleteModalState: DeleteModalState;
|
||||
@@ -1399,6 +1402,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: '',
|
||||
pendingAddMediaUrls: [],
|
||||
pendingAddTorrentUrls: [],
|
||||
pendingAddBatch: false,
|
||||
pendingAddBatchName: '',
|
||||
pendingAddRequestContexts: {},
|
||||
@@ -1420,6 +1424,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: '',
|
||||
pendingAddMediaUrls: [],
|
||||
pendingAddTorrentUrls: [],
|
||||
pendingAddBatch: false,
|
||||
pendingAddBatchName: '',
|
||||
pendingAddRequestContexts: {},
|
||||
@@ -1436,7 +1441,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
media = false,
|
||||
cookieScopes,
|
||||
batch = false,
|
||||
batchName
|
||||
batchName,
|
||||
torrent = false
|
||||
) => set((state) => {
|
||||
const isAppending = state.isAddModalOpen && Boolean(state.pendingAddUrls);
|
||||
const existingUrls = isAppending ? state.pendingAddUrls : '';
|
||||
@@ -1482,12 +1488,16 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
headers: cleanHeaders,
|
||||
cookies: cleanCookies,
|
||||
...(cleanCookieScopes?.length ? { cookieScopes: cleanCookieScopes } : {}),
|
||||
media
|
||||
media,
|
||||
...(torrent ? { torrent: true } : {})
|
||||
};
|
||||
}
|
||||
const pendingAddMediaUrls = Object.entries(pendingAddRequestContexts)
|
||||
.filter(([, context]) => context.media)
|
||||
.map(([url]) => url);
|
||||
const pendingAddTorrentUrls = Object.entries(pendingAddRequestContexts)
|
||||
.filter(([, context]) => context.torrent)
|
||||
.map(([url]) => url);
|
||||
return {
|
||||
isAddModalOpen: true,
|
||||
pendingAddUrls: mergedUrls,
|
||||
@@ -1496,6 +1506,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
pendingAddHeaders: cleanHeaders,
|
||||
pendingAddCookies: cleanCookies,
|
||||
pendingAddMediaUrls,
|
||||
pendingAddTorrentUrls,
|
||||
pendingAddBatch: nextBatch,
|
||||
pendingAddBatchName: nextBatchName,
|
||||
pendingAddRequestContexts,
|
||||
@@ -1523,7 +1534,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
request.media === true,
|
||||
request.media === true ? undefined : request.cookie_scopes,
|
||||
request.batch === true && urls.length >= 2,
|
||||
request.batch_name
|
||||
request.batch_name,
|
||||
request.torrent === true
|
||||
);
|
||||
},
|
||||
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
||||
|
||||
@@ -120,6 +120,29 @@ describe('add download metadata workflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an explicit torrent handoff for an opaque remote URL', () => {
|
||||
const sourceUrl = 'https://example.com/download?id=opaque';
|
||||
const rows = reconcileDownloadRows(
|
||||
sourceUrl,
|
||||
[],
|
||||
'example.torrent',
|
||||
new Set(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
new Set([sourceUrl])
|
||||
);
|
||||
|
||||
expect(rows[0]).toMatchObject({
|
||||
sourceUrl,
|
||||
isTorrent: true,
|
||||
torrentCacheId: `${rows[0].id}-1`,
|
||||
file: 'example.torrent'
|
||||
});
|
||||
});
|
||||
|
||||
it('gives refreshed torrent metadata a new cache identity', () => {
|
||||
const existing = row({
|
||||
id: 'torrent-row',
|
||||
|
||||
@@ -235,7 +235,8 @@ export const reconcileDownloadRows = (
|
||||
requestFilenames: Readonly<Record<string, string>> = {},
|
||||
requestContextVersions: Readonly<Record<string, number>> = {},
|
||||
playlistExpansions: PlaylistExpansions = {},
|
||||
selectedBySourceUrl: Readonly<Record<string, boolean>> = {}
|
||||
selectedBySourceUrl: Readonly<Record<string, boolean>> = {},
|
||||
forceTorrentUrls: ReadonlySet<string> = new Set()
|
||||
): AddDownloadDraftRow[] => {
|
||||
const inputs = parseInputLines(
|
||||
rawText,
|
||||
@@ -249,6 +250,7 @@ export const reconcileDownloadRows = (
|
||||
const preserved = existing.get(input.sourceUrl);
|
||||
if (preserved) {
|
||||
const forcedMedia = input.valid && forceMediaUrls.has(input.sourceUrl);
|
||||
const forcedTorrent = input.valid && forceTorrentUrls.has(input.sourceUrl);
|
||||
const requestContextVersion = input.requestContextVersion;
|
||||
const contextChanged = requestContextVersion !== undefined
|
||||
&& requestContextVersion !== preserved.requestContextVersion;
|
||||
@@ -257,7 +259,10 @@ export const reconcileDownloadRows = (
|
||||
|| preserved.playlistIndex !== input.playlistIndex
|
||||
|| preserved.playlistCount !== input.playlistCount
|
||||
|| preserved.playlistEntryTitle !== input.playlistEntryTitle;
|
||||
if ((forcedMedia && !preserved.isMedia) || contextChanged || playlistContextChanged) {
|
||||
if ((forcedMedia && !preserved.isMedia)
|
||||
|| (forcedTorrent && !preserved.isTorrent)
|
||||
|| contextChanged
|
||||
|| playlistContextChanged) {
|
||||
const nextGeneration = preserved.generation + 1;
|
||||
const requestedFilename = input.playlistSourceUrl
|
||||
? `${playlistFilePrefix(input.playlistIndex, input.playlistCount)}${input.playlistEntryTitle || 'video'}`
|
||||
@@ -271,7 +276,7 @@ export const reconcileDownloadRows = (
|
||||
generation: nextGeneration,
|
||||
requestContextVersion,
|
||||
isMedia: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl),
|
||||
isTorrent: input.isTorrent,
|
||||
isTorrent: input.isTorrent || forcedTorrent,
|
||||
size: undefined,
|
||||
sizeBytes: undefined,
|
||||
resumable: undefined,
|
||||
@@ -290,7 +295,7 @@ export const reconcileDownloadRows = (
|
||||
playlistError: undefined,
|
||||
metadataBlockedReason: undefined,
|
||||
torrentPath: undefined,
|
||||
torrentCacheId: input.isTorrent ? `${preserved.id}-${nextGeneration}` : undefined,
|
||||
torrentCacheId: input.isTorrent || forcedTorrent ? `${preserved.id}-${nextGeneration}` : undefined,
|
||||
torrentInfoHash: undefined,
|
||||
torrentFiles: undefined,
|
||||
selectedTorrentFileIndices: undefined
|
||||
@@ -323,7 +328,7 @@ export const reconcileDownloadRows = (
|
||||
|| forceMediaUrls.has(input.sourceUrl)
|
||||
|| isMediaUrl(input.sourceUrl)
|
||||
),
|
||||
isTorrent: input.valid && Boolean(input.isTorrent),
|
||||
isTorrent: input.valid && (Boolean(input.isTorrent) || forceTorrentUrls.has(input.sourceUrl)),
|
||||
isPlaylist: input.isPlaylist,
|
||||
playlistSourceUrl: input.playlistSourceUrl,
|
||||
playlistTitle: input.playlistTitle,
|
||||
@@ -331,7 +336,9 @@ export const reconcileDownloadRows = (
|
||||
playlistCount: input.playlistCount,
|
||||
playlistEntryTitle: input.playlistEntryTitle,
|
||||
metadataBlockedReason: undefined,
|
||||
torrentCacheId: input.valid && input.isTorrent ? `${id}-${generation}` : undefined,
|
||||
torrentCacheId: input.valid && (input.isTorrent || forceTorrentUrls.has(input.sourceUrl))
|
||||
? `${id}-${generation}`
|
||||
: undefined,
|
||||
selected: input.selected !== false
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user