import bencode from 'bencode' ; import omit from 'lodash/omit.js' ; import sort_by from 'lodash/sortBy.js' ; import { fread , frename , fwrite , fmove , inspect , flist } from 'xshell' ; export class TorrentData { fp_torrent : string ; /** 种子任务名称 */ caption : string ; /** 错误说明，如：任务文件丢失。请重新检查。 */ dl_error : string ; /** 若种子文件只包含一个文件，则 path 为该文件的完整存储路径，
        否则 path 为文件夹的位置
     */ path : string ; /** 高级 > 设置目标名称
        文件位置的重定位表，格式为 [index, fp][]，未重定位则无此字段
        index: 种子文件的文件列表所对应的文件序号 (从 0 开始编号)
        fp:    文件重定位后的相对 path 的路径
        
        比如：
        0  : 1.mkv
        2  : 3.mkv
        351: SPs/24-blabla.mkv
    */ targets ? : [ number , string ] [ ] ; downloaded : number ; uploaded : number ; blocksize : number ; trackers : string [ ] ; labels : string [ ] ; started : number ; valid : number ; have : Buffer ; hashed : Buffer ; known : Buffer ; [ key ] : any ; constructor ( fp_torrent , data ) { this . fp_torrent = fp_torrent . to_slash ( ) ; Object . entries ( data ) . forEach ( ( [ key , value ] ) => { if ( key === 'caption' || key === 'dl_error' ) { this [ key ] = ( value as Buffer ) . toString ( ) ; return ; } if ( key === 'trackers' || key === 'labels' ) { this [ key ] = ( value as Buffer [ ] ) . map ( buf => buf . toString ( ) ) ; return ; } if ( key === 'targets' ) { this [ key ] = ( value as [ number , Buffer ] [ ] ) . map ( ( [ index , fp ] ) => ( [ index , fp . toString ( ) . to_slash ( ) ] ) ) ; return ; } if ( key === 'path' ) { this [ key ] = ( value as Buffer ) . toString ( ) . to_slash ( ) ; return ; } this [ key ] = value ; } ) ; } to_str ( ) { const title_width = 14 ; return `${ this . caption }\n` + [ `${ '种子:' . pad ( title_width ) }${ this . fp_torrent }` , `${ '文件夹:' . pad ( title_width ) }${ this . path }` , `${ '标签:' . pad ( title_width ) }${ this . labels . join ( ', ' ) }` , `${ '已下载:' . pad ( title_width ) }${ this . downloaded . to_fsize_str ( ) }    ／    已上传: ${ this . uploaded . to_fsize_str ( ) }    分块大小: ${ this . blocksize . to_fsize_str ( ) }` , ... ! this . valid ? [ `${ '错误:' . pad ( title_width ) }${ this . dl_error || '' }` ] : [ ] , `${ 'trackers:' . pad ( title_width ) }Array<${ this . trackers . length }> [${ this . trackers . slice ( 0 , 2 ) . join ( ', ' ) }]` , ... this . targets ? [ '文件重定位:' , ... this . targets . map ( ( [ index , fp ] ) => `${ index }: ${ fp }` ) . indent ( 2 ) , ] : [ ] , ] . indent ( 2 ) . join_lines ( ) ; } [ inspect . custom ] ( ) { return this . to_str ( ) ; } to_data ( ) { const data = omit ( this , [ 'fp_torrent' ] ) ; return { ... data , ... this . targets ? { targets : this . targets . map ( ( [ index , fp ] ) => ( [ index , fp . to_backslash ( ) ] ) ) } : { } , path : this . path . to_backslash ( ) , } ; } relocate_torrent ( fp_torrent ) { if ( ! fp_torrent . fexists ) throw new Error ( `fp_torrent 文件不存在: ${ fp_torrent }` ) ; this . fp_torrent = fp_torrent . to_backslash ( ) ; } fix ( ) { this . started = 0 ; this . valid = 1 ; this . have . fill ( 0xff ) ; this . hashed . fill ( 0xff ) ; this . known . fill ( 0xff ) ; } } export class TorrentFile { info : { files ? : { length : number ; path : string ; } [ ] ; name : string ; 'piece length' : number ; pieces : Buffer ; } ; private constructor ( data ) { Object . assign ( this , data ) ; } static async parse ( fp ) { const data = bencode . decode ( await fread ( fp , { encoding : 'BINARY' } ) ) ; const files = data . info . files ; return new TorrentFile ( { ... data , info : { ... data . info , ... files ? { files : files . map ( file => ( { length : file . length , path : file . path . map ( ( fp ) => fp . toString ( ) ) . join ( '/' ) } ) ) } : { } } } ) ; } get_fps ( ) { return this . info . files . map ( file => file . path ) ; } [ inspect . custom ] ( ) { return [ 'TorrentFile {' , ... [ 'files:' , ... this . get_fps ( ) . map ( ( fp , i ) => i . toString ( ) . pad ( 4 ) + ': ' + fp ) . indent ( 2 ) , '' ] . indent ( 2 ) , '}' , ] . join_lines ( ) ; } } export class ResumeData { fp_resume_dat : string ; rec : Buffer ; torrents : TorrentData [ ] ; private constructor ( data ) { Object . assign ( this , data ) ; } static async parse ( fp_resume_dat ) { const data = bencode . decode ( await fread ( fp_resume_dat , { encoding : 'BINARY' } ) ) ; return new ResumeData ( { fp_resume_dat , rec : data . rec , torrents : sort_by ( Object . entries ( data ) . filter ( ( [ key ] ) => key !== 'rec' && key !== '.fileguard' ) . map ( ( [ fp_torrent , data ] ) => new TorrentData ( fp_torrent , data ) ) , 'caption' ) } ) ; } async save ( fp = this . fp_resume_dat ) { await fwrite ( fp , bencode . encode ( { rec : this . rec , ... Object . fromEntries ( this . torrents . map ( torrent => ( [ torrent . fp_torrent . to_backslash ( ) , torrent . to_data ( ) ] ) ) ) } ) ) ; } relocate_paths ( src , dest , filter ? ) { } /** 根据 pattern 查找首个匹配的种子，不传 pattern 参数时返回最近添加的种子 */ find_torrent ( pattern ? ) { if ( ! pattern ) return sort_by ( this . torrents , 'added_on' ) . last ; return this . torrents . find ( torrent => torrent . caption . match ( pattern ) ) ; } find_torrents ( pattern ) { return this . torrents . filter ( torrent => torrent . caption . match ( pattern ) ) ; } [ inspect . custom ] ( ) { return 'ResumeData {\n' + [ `fp_resume_dat: ${ this . fp_resume_dat }\n` , ... this . torrents . map ( torrent => { return [ ... torrent . to_str ( ) . split_lines ( ) , '' ] ; } ) ] . flat ( ) . indent ( 2 ) . join_lines ( ) + '\n' + '}' ; } } export default ResumeData ; async function repl_rename_onepice ( ) { const dir = 'G:/ACGN/海贼王/' ; let resume_data = await ResumeData . parse ( 'C:/Users/shf/AppData/Roaming/uTorrent/resume.dat' ) ; let torrents = sort_by ( resume_data . torrents , 'added_on' ) . reverse ( ) . slice ( 0 , 5 ) ; let torrent = torrents [ 0 ] ; const fp_torrent = torrent . fp_torrent ; const episode = torrent . caption . find ( '[Skytree][海贼王][One_Piece][{episode}][GB_BIG5_JP][X264_AAC][1080P][CRRIP][天空树双语字幕组]' ) . episode ; const fp_mkv = torrent . path ; const fp_torrent_ = `${ dir }${ episode }.torrent` ; await fmove ( fp_torrent , fp_torrent_ ) ; await fmove ( fp_mkv , `${ dir }${ episode }.mkv` ) ; torrent . caption = `海贼王 ${ episode }` ; torrent . fp_torrent = `${ dir }${ episode }.torrent` ; torrent . path = `${ dir }${ episode }.mkv` ; torrent . labels = [ '海贼王' ] ; await resume_data . save ( ) ; } async function repl_resume_dat ( ) { let resume_data = await ResumeData . parse ( 'C:/Users/shf/AppData/Roaming/uTorrent/resume.dat' ) ; let torrents = resume_data . torrents . filter ( ( { caption , labels } ) => labels . includes ( '电影' ) ) ; let torrent = torrents [ 0 ] ; let torrent_pt = sort_by ( resume_data . torrents , 'added_on' ) . reverse ( ) [ 0 ] ; const { fp_torrent , caption } = torrent ; const name = '妹さえいればいい。' ; const dir = `G:/ACGN/${ name }/` ; let version = '' ; const info = 'BDRip 1080p x265 Ma10p FLAC' ; const origin = 'VCB-Studio' ; const caption_ = name + ( version ? ` ${ version . bracket ( 'ROUND' ) }` : '' ) + ( info ? ` ${ info . bracket ( 'SQUARE' ) }` : '' ) + ( origin ? ` ${ origin . bracket ( 'SQUARE' ) }` : '' ) ; const fp_torrent_ = `${ dir }${ caption_ }.torrent` ; await fmove ( fp_torrent , fp_torrent_ ) ; torrent . caption = caption_ ; torrent . fp_torrent = fp_torrent_ ; torrent . path = dir ; torrent . labels = [ 'ACGN' ] ; const ftorrent = await TorrentFile . parse ( torrent . fp_torrent ) ; const ftorrent_pt = await TorrentFile . parse ( fp_torrent . replace ( '.torrent' , '.pt.torrent' ) ) ; ftorrent . info . files . forEach ( ( { path } , i ) => { console . log ( `${ String ( i ) . pad ( 3 , { character : '0' , position : 'left' } ) . green } ${ path }` ) ; } ) ; torrent . targets = [ ... ftorrent . get_fps ( ) . slice ( 58 ) . map ( ( fp , index ) => ( [ 58 + index , fp . refmt ( '[VCB-S]Hanamonogatari[1080p]/{any}' , '花物語/{any}' ) ] ) ) , ] as [ number , string ] [ ] ; let fps = await flist ( 'G:/ACGN/猫和老鼠/' , { absolute : false } ) ; fps = fps . slice ( 37 ) ; torrent . targets = fps . map ( ( fp , i ) => ( [ 37 + i , fp ] ) ) ; await Promise . all ( [ ftorrent . get_fps ( ) . slice ( 0 , 37 ) . map ( async ( fp , index ) => { const fp_ = fp . refmt ( 'TOM AND JERRY EP{episode} {title}.mkv' , '{episode}   {title}.mkv' ) ; await frename ( fp , fp_ , { dir } ) ; } ) ] ) ; const text = '' ; await Promise . all ( [ text . split_lines ( ) . trim_lines ( ) . map ( async ( fp ) => { const fp_ = fp . refmt ( '{episode}   {title}.mkv' , '0{episode}   {title}.mkv' ) ; await frename ( fp , fp_ , { dir } ) ; } ) ] ) ; torrent_pt . caption = torrent . caption + '.pt' ; torrent_pt . fp_torrent = torrent . fp_torrent . replace ( '.torrent' , '.pt.torrent' ) ; torrent_pt . path = torrent . path ; torrent_pt . labels = torrent . labels ; torrent_pt . targets = torrent . targets ; torrent ; torrent_pt ; resume_data ; sort_by ( resume_data . torrents , 'added_on' ) . reverse ( ) . slice ( 0 , 5 ) ; await resume_data . save ( ) ; }