File size: 4,638 Bytes
1fa2c88 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
import crypto from 'crypto';
import path from 'path';
import { writeFile, readFile, mkdir, readdir, unlink, stat } from 'node:fs/promises';
import parseTorrent from 'parse-torrent';
import {toMagnetURI} from 'parse-torrent';
import cache from './cache.js';
import config from './config.js';
const TORRENT_FOLDER = `${config.dataFolder}/torrents`;
const CACHE_FILE_DAYS = 7;
export async function createTorrentFolder(){
return mkdir(TORRENT_FOLDER).catch(() => false);
}
export async function cleanTorrentFolder(){
const files = await readdir(TORRENT_FOLDER);
const expireTime = new Date().getTime() - 86400*CACHE_FILE_DAYS*1000;
for (const file of files) {
if(!file.endsWith('.torrent'))continue;
const filePath = path.join(TORRENT_FOLDER, file);
const stats = await stat(filePath);
if(stats.ctimeMs < expireTime){
await unlink(filePath);
}
}
}
export async function get({link, id, magnetUrl, infoHash, name, size, type}){
try {
return await getById(id);
}catch(err){}
let parseInfos = null;
let torrentLocation = '';
if(magnetUrl && infoHash && name && size > 0 && type){
parseInfos = {
infoHash,
name,
length: size,
private: (type == 'private')
};
}else{
if(link.startsWith('http')){
try {
torrentLocation = `${TORRENT_FOLDER}/${id}.torrent`;
const buffer = await downloadTorrentFile({link, id, torrentLocation});
parseInfos = await parseTorrent(new Uint8Array(buffer));
if(!parseInfos.private){
magnetUrl = toMagnetURI(parseInfos);
}
}catch(err){
torrentLocation = '';
if(err.redirection && err.redirection.startsWith('magnet')){
link = err.redirection;
}else{
// Add indexer info to error message if available
const errorMessage = err.message + (err.indexerId ? ` (Indexer: ${err.indexerId})` : '');
throw new Error(errorMessage);
}
}
}
if(link.startsWith('magnet')){
parseInfos = await parseTorrent(link);
magnetUrl = link;
}
}
if(!parseInfos){
throw new Error(`Invalid link ${link}`);
}
const torrentInfos = {
id,
link,
magnetUrl: magnetUrl || '',
torrentLocation,
infoHash: (parseInfos.infoHash || '').toLowerCase(),
name: parseInfos.name || '',
private: parseInfos.private || false,
size: parseInfos.length || -1,
files: (parseInfos.files || []).map(file => {
return {
name: file.name,
size: file.length
}
})
};
await setById(id, torrentInfos);
return torrentInfos;
}
export async function getById(id){
const cacheKey = `torrentInfos:${id}`;
const infos = await cache.get(cacheKey);
if(!infos){
throw new Error(`Torrent infos cache seem expired for id ${id}`);
}
return infos;
}
async function setById(id, infos){
const cacheKey = `torrentInfos:${id}`;
await cache.set(cacheKey, infos, {ttl: 86400*CACHE_FILE_DAYS});
return infos;
}
export async function getTorrentFile(infos){
if(infos.torrentLocation){
try {
return await readFile(infos.torrentLocation);
}catch(err){}
}
return downloadTorrentFile(infos);
}
async function downloadTorrentFile({link, id, torrentLocation, indexerId}){
const res = await fetch(link, {redirect: 'manual'});
// Handle redirections
if(res.headers.has('location')){
throw Object.assign(new Error(`Redirection detected ...`), {redirection: res.headers.get('location')});
}
const contentType = res.headers.get('content-type') || '';
// Check if we got an HTML response (usually an error page)
if(contentType.includes('text/html')){
const htmlContent = await res.text();
let errorMessage = 'Site returned an error page';
// Try to extract meaningful error messages
if(htmlContent.includes('ratio is dangerously low')){
errorMessage = 'Download blocked due to low ratio';
}else if(htmlContent.includes('do not have permission')){
errorMessage = 'Permission denied';
}
// Add indexer information to error
throw Object.assign(new Error(errorMessage), { indexerId });
}
// Verify we got a torrent file
if(!contentType.includes('application/x-bittorrent')){
throw Object.assign(
new Error(`Invalid content-type: ${contentType}`),
{ indexerId }
);
}
if(res.status != 200){
throw Object.assign(
new Error(`Invalid status: ${res.status}`),
{ indexerId }
);
}
const buffer = await res.arrayBuffer();
writeFile(torrentLocation, new Uint8Array(buffer));
return buffer;
} |