diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 416472d7..bb4180c4 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -1,6 +1,6 @@ import { BaseWrapper, Torbox, Torrentio } from '@aiostreams/wrappers'; import { Stream, ParsedStream, StreamRequest, Config } from '@aiostreams/types'; -import { gdriveFormat } from '@aiostreams/formatters'; +import { gdriveFormat, torrentioFormat } from '@aiostreams/formatters'; export class AIOStreams { private config: Config; @@ -84,6 +84,13 @@ export class AIOStreams { description = _description; break; } + case 'torrentio': { + const { name: _name, description: _description } = + torrentioFormat(parsedStream); + name = _name; + description = _description; + break; + } default: { throw new Error('Unsupported formatter'); } diff --git a/packages/formatters/src/index.ts b/packages/formatters/src/index.ts index 6e61e713..b426d297 100644 --- a/packages/formatters/src/index.ts +++ b/packages/formatters/src/index.ts @@ -1,2 +1,3 @@ export * from './gdrive'; export * from './utils'; +export * from './torrentio'; diff --git a/packages/formatters/src/torrentio.ts b/packages/formatters/src/torrentio.ts new file mode 100644 index 00000000..3065edde --- /dev/null +++ b/packages/formatters/src/torrentio.ts @@ -0,0 +1,46 @@ +import { ParsedStream } from '@aiostreams/types'; +import { formatSize } from './utils'; + +export function torrentioFormat(stream: ParsedStream): { + name: string; + description: string; +} { + let name: string = ''; + + if (stream.provider) { + name += stream.provider.cached + ? `[${stream.provider.name}+]\n` + : `[${stream.provider.name} download]\n`; + } + + name += `${stream.addonName} ${stream.resolution} `; + + if (stream.visualTags.length > 0) { + name += stream.visualTags.join(' | '); + } + + let description = stream.filename; + if ( + stream.size || + stream.torrent?.seeders || + stream.usenet?.age || + stream.indexers + ) { + description += '\n'; + + description += stream.torrent?.seeders + ? `👤 ${stream.torrent.seeders} ` + : ''; + + description += stream.usenet?.age ? `📅 ${stream.usenet.age}d ` : ''; + + description += `💾 ${formatSize(stream.size || 0)} `; + + description += stream.indexers ? `⚙️ ${stream.indexers} ` : ''; + } + if (stream.languages.length !== 0) { + description += `\n${stream.languages.join(' / ')}`; + } + + return { name, description }; +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index cf47220b..0d94bc16 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -9,18 +9,18 @@ "lint": "next lint" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", + "@aiostreams/types": "^1.0.0", "next": "15.1.2", - "@aiostreams/types": "^1.0.0" + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.2", - "@eslint/eslintrc": "^3" + "typescript": "^5" } } diff --git a/packages/frontend/src/app/page.module.css b/packages/frontend/src/app/page.module.css index 7b6ef8d4..6b3fabbb 100644 --- a/packages/frontend/src/app/page.module.css +++ b/packages/frontend/src/app/page.module.css @@ -176,6 +176,26 @@ a.secondary { gap: 8px; } +.buttonGroup { + display: flex; + gap: 16px; + margin-top: 16px; +} + +.configButton { + padding: 10px 20px; + font-size: 16px; + background-color: var(--foreground); + color: var(--background); + border: none; + border-radius: 4px; + cursor: pointer; +} + +.configButton:hover { + background-color: var(--button-primary-hover); +} + /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { a.primary:hover { diff --git a/packages/frontend/src/app/page.tsx b/packages/frontend/src/app/page.tsx index 6c9fb81e..a30d927c 100644 --- a/packages/frontend/src/app/page.tsx +++ b/packages/frontend/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import styles from './page.module.css'; const predefinedResolutions = ['2160p', '1080p', '720p', '480p', 'Unknown']; @@ -66,6 +66,18 @@ export default function Configure() { const [selectedDebridService, setSelectedDebridService] = useState(''); + useEffect(() => { + const path = window.location.pathname; + const configMatch = path.match(/\/([^/]+)\/configure/); + if (configMatch) { + const decodedConfig = JSON.parse(atob(configMatch[1])); + setConfig((prevConfig) => ({ + ...prevConfig, + ...decodedConfig, + })); + } + }, []); + const handleSubmit = (e: any) => { e.preventDefault(); const filteredConfig = { @@ -75,7 +87,11 @@ export default function Configure() { ), }; const encodedConfig = btoa(JSON.stringify(filteredConfig)); - window.location.href = `/${encodedConfig}/manifest.json`; + const protocol = window.location.protocol; + const host = window.location.host; + const link = `${protocol}//${host}/${encodedConfig}/manifest.json`; + navigator.clipboard.writeText(link); + alert('Link copied to clipboard!'); }; const handleChange = (e: any) => { @@ -105,6 +121,47 @@ export default function Configure() { })); }; + const handleCopyLink = () => { + const filteredConfig = { + ...config, + apiKeys: Object.fromEntries( + Object.entries(config.apiKeys).filter(([_, value]) => value) + ), + }; + const encodedConfig = btoa(JSON.stringify(filteredConfig)); + const protocol = window.location.protocol; + const host = window.location.host; + const link = `${protocol}//${host}/${encodedConfig}/manifest.json`; + navigator.clipboard.writeText(link); + alert('Link copied to clipboard!'); + }; + + const handleInstallToWeb = () => { + const filteredConfig = { + ...config, + apiKeys: Object.fromEntries( + Object.entries(config.apiKeys).filter(([_, value]) => value) + ), + }; + const encodedConfig = btoa(JSON.stringify(filteredConfig)); + const protocol = window.location.protocol; + const host = window.location.host; + const link = `https://web.stremio.com/#/addons?addon=${protocol}//${host}/${encodedConfig}/manifest.json`; + window.location.href = link; + }; + + const handleInstall = () => { + const filteredConfig = { + ...config, + apiKeys: Object.fromEntries( + Object.entries(config.apiKeys).filter(([_, value]) => value) + ), + }; + const encodedConfig = btoa(JSON.stringify(filteredConfig)); + const link = `stremio://${encodedConfig}/manifest.json`; + window.location.href = link; + }; + return (