Skip to content

Instantly share code, notes, and snippets.

@tarruda
Created December 23, 2023 09:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tarruda/1973d317f6195189828492af33679cc1 to your computer and use it in GitHub Desktop.
Save tarruda/1973d317f6195189828492af33679cc1 to your computer and use it in GitHub Desktop.
Deluge torrent bulk renamer
#!/usr/bin/env python3
# This script does batch renaming of torrents served by deluge. Useful to
# quickly rename all episodes of a TV show to match the expected patterns of
# local streaming services such as Plex or Jellyfin while still seeding the
# torrent.
# For example, consider you have a TV show that has a strange name not
# recognized by jellyfin, just use a regex replace to capture the episode
# number and rename to a pattern understood by Jellyfin:
# python3 bulk-rename.py -d DELUGE_HOST -U USER -P PASSWORD TORRENT_HASH '(?P<dir>[^/]+)/.+ep\.(?P<episode>\d+)(?P<suffix>.+)$' '\g<dir>/TV.Show.Name.s01.e\g<episode>\g<suffix>'
import re
import argparse
import sys
from deluge.ui.client import client
from twisted.internet import reactor
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--daemon-host', '-d', default='127.0.0.1',
help='Deluge daemon host')
parser.add_argument('--daemon-port', '-p', default='58846', type=int,
help='Deluge daemon port')
parser.add_argument('--daemon-username', '-U',
help='Deluge daemon username')
parser.add_argument('--daemon-password', '-P',
help='Deluge daemon password')
parser.add_argument('torrent_id',
help='Torrent hash')
parser.add_argument('regex',
help='Regexp to match filenames')
parser.add_argument('repl',
help='Replacement, which can reference captures')
return parser.parse_args()
def rename(host,
port,
username,
password,
torrent_id,
pattern,
repl):
connect = client.connect(host,
port=port,
username=username,
password=password)
def on_connect(result):
def on_get_torrent(torrent):
renames_preview = []
for file in torrent['files']:
m = pattern.match(file['path'])
if m:
renames_preview.append({
'index': file['index'],
'src': file['path'],
'dst':re.sub(pattern, repl, file['path'])
})
if not renames_preview:
return error('No files matched')
target_set = set()
print('Will rename:')
for p in renames_preview:
print(p['src'], '->', p['dst'])
target_set.add(p['dst'])
src_len = len(renames_preview)
tgt_len = len(target_set)
if src_len != tgt_len:
return error(
f'Target count({tgt_len}) != Source count({src_len})')
def on_rename(result):
print('Rename was successful')
client.disconnect()
reactor.stop()
def on_rename_failure(result):
return error(f'Failed to rename: {str(result)}')
answer = input("Confirm? (y/N)").lower()
if answer in ['y', 'Y']:
renames = list((p['index'], p['dst']) for p in renames_preview)
rename_files = client.core.rename_files(torrent_id, renames)
rename_files.addCallback(on_rename)
rename_files.addErrback(on_rename_failure)
def on_get_torrent_error(err):
print('failed to get torrent status:', err)
client.disconnect()
reactor.stop()
get_torrent_status = client.core.get_torrent_status(torrent_id, ["files"])
get_torrent_status.addCallback(on_get_torrent)
get_torrent_status.addErrback(on_get_torrent_error)
connect.addCallback(on_connect)
def on_connect_fail(result):
print("Connection failed!")
print("result:", result)
reactor.stop()
connect.addErrback(on_connect_fail)
reactor.run()
def error(message):
print(message, file=sys.stderr)
client.disconnect()
reactor.stop()
def main():
args = parse_args()
pattern = re.compile(args.regex)
rename(
args.daemon_host,
args.daemon_port,
args.daemon_username,
args.daemon_password,
args.torrent_id,
pattern,
args.repl)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment