├── .gitignore ├── README.md ├── docs ├── api-checkboxes.png └── api-done.png ├── gazelleorigin ├── __init__.py ├── __main__.py └── core.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gazelle-origin 2 | ============== 3 | 4 | `gazelle-origin` is a script that fetches and saves YAML torrent origin information from Gazelle-based music trackers. 5 | 6 | Example output from `gazelle-origin`: 7 | 8 | ~~~ 9 | Artist: Pink Floyd 10 | Name: The Dark Side of the Moon 11 | Edition: 'Japan MFSL UltraDisc #1, 24 Karat Gold' 12 | Edition year: 1988 13 | Media: CD 14 | Catalog number: UDCD 517 15 | Record label: Mobile Fidelity Sound Lab 16 | Original year: 1973 17 | Format: FLAC 18 | Encoding: Lossless 19 | Log: 70% 20 | Directory: Pink Floyd - Dark Side of the Moon (OMR MFSL 24k Gold Ultradisc II) fixed tags 21 | Size: 219114079 22 | File count: 12 23 | Info hash: C380B62A3EC6658597C56F45D596E8081B3F7A5C 24 | Uploaded: 2016-11-24 01:34:03 25 | Permalink: https://redacted.ch/torrents.php?torrentid=1 26 | 27 | Comment: |- 28 | [important]Staff: Technically trumped because EAC 0.95 logs are terrible. There is historic and sentimental value in keeping the first torrent ever uploaded to the site as well as a perfect modern rip. Take no action.[/important] 29 | 30 | Files: 31 | - Name: 01 - Speak to Me.flac 32 | Size: 3732587 33 | - Name: 02 - Breathe.flac 34 | Size: 14244409 35 | - Name: 03 - On the Run.flac 36 | Size: 16541873 37 | - Name: 04 - Time.flac 38 | Size: 35907465 39 | - Name: 05 - The Great Gig in the Sky.flac 40 | Size: 20671913 41 | - Name: 06 - Money.flac 42 | Size: 37956922 43 | - Name: 07 -Us and Them.flac 44 | Size: 39706774 45 | - Name: 08 - Any Colour You Like.flac 46 | Size: 18736396 47 | - Name: 09 - Brain Damage.flac 48 | Size: 20457034 49 | - Name: 10 - Eclipse.flac 50 | Size: 11153655 51 | - Name: Pink Floyd - Dark Side of the Moon.CUE 52 | Size: 1435 53 | - Name: Pink Floyd - Dark Side of the Moon.log 54 | Size: 3616 55 | ~~~ 56 | 57 | Motivation 58 | ---------- 59 | 60 | Having origin information locally available for each downloaded torrent has a number of benefits: 61 | * music can be retagged and renamed without losing immediate access to original metadata, 62 | * if the tracker is ever down or goes away, the origin information is still available, and 63 | * origin information can be passed to other scripts/tools (e.g., beets) to more accurately identify your music (see 64 | [beets integration](#beets)). 65 | 66 | While some uploaders helpfully include this information in their uploads, this 67 | is far from standard practice. Additionally, using a tool like `gazelle-origin` 68 | means all torrents will have consistent, parseable origin data independent of 69 | uploader formatting. 70 | 71 | Supported Trackers 72 | ------------------ 73 | 74 | Currently, only redacted.ch is supported. Use `--tracker red` or set the `ORIGIN_TRACKER=red` environment variable to 75 | use it. 76 | 77 | Installation 78 | ------------ 79 | 80 | Install using `pip`: 81 | 82 | $> pip install git+https://github.com/x1ppy/gazelle-origin 83 | 84 | Then add your tracker API key (see [Obtaining Your API Key](https://github.com/x1ppy/gazelle-origin#obtaining-your-api-key)) to `~/.bashrc` or equivalent: 85 | 86 | export RED_API_KEY= 87 | 88 | Though not required, it's also recommended that you add a default tracker to `~/.bashrc` or equivalent (see [Supported Trackers](#supported-trackers)): 89 | 90 | export ORIGIN_TRACKER= 91 | 92 | And reload it: 93 | 94 | $> source ~/.bashrc 95 | 96 | Finally, see [Integration](#torrent-clients) for calling `gazelle-origin` automatically from your torrent client. 97 | 98 | Obtaining Your API Key 99 | --------------------- 100 | `gazelle-origin` requires an API key to make API requests. To obtain your API key: 101 | 102 | ### redacted.ch 103 | * Go to your profile and select Access Settings on the right side 104 | * Scroll down to API Keys 105 | * Enter "gazelle-origin" as the name 106 | * Uncheck all boxes except Torrents 107 | * Copy all of the text in the Key: box (this is your API key) 108 | * Check Confirm API Key and save 109 | 110 | Before saving, the fields should look like this: 111 | ![before saving](docs/api-checkboxes.png "Before saving") 112 | 113 | After saving, you should see a Torrents API key like this: 114 | ![after saving](docs/api-done.png "After saving") 115 | 116 | Usage 117 | ----- 118 | 119 | ~~~ 120 | usage: gazelle-origin [-h] [--out file] [--tracker tracker] [--env file] 121 | [--post file [file ...]] [--recursive] [--no-hash] 122 | torrent [torrent ...] 123 | 124 | Fetches torrent origin information from Gazelle-based music trackers 125 | 126 | positional arguments: 127 | torrent torrent identifier, which can be either its info hash, 128 | torrent ID, permalink, or path to torrent file(s) 129 | whose name or computed info hash should be used 130 | 131 | optional arguments: 132 | -h, --help show this help message and exit 133 | --out file, -o file path to write origin data (default: print to stdout) 134 | --tracker tracker, -t tracker 135 | tracker to use 136 | --env file, -e file file to load environment variables from 137 | --post file [file ...], -p file [file ...] 138 | script(s) to run after each output is written. These 139 | scripts have access to environment variables with info 140 | about the item including OUT, ARTIST, NAME, DIRECTORY, 141 | EDITION, YEAR, FORMAT, ENCODING 142 | --recursive, -r recursively search directories for files 143 | --no-hash, -n don't compute hash from torrent files 144 | 145 | --tracker is optional if the ORIGIN_TRACKER environment variable is set. 146 | 147 | If provided, --tracker must be set to one of the following: red 148 | ~~~ 149 | 150 | Examples 151 | -------- 152 | 153 | These examples all assume you have the `ORIGIN_TRACKER` environment variable set as described in 154 | [Installation](#Installation). If you don't, or if you want to use a different tracker, include the `--tracker` flag in 155 | the following commands. 156 | 157 | To show origin information for a given torrent using its info hash: 158 | 159 | $> gazelle-origin C380B62A3EC6658597C56F45D596E8081B3F7A5C 160 | 161 | Alternatively, you can pass the permalink instead of the info hash: 162 | 163 | $> gazelle-origin "https://redacted.ch/torrents.php?torrentid=1" 164 | 165 | You can even supply just the torrent ID: 166 | 167 | $> gazelle-origin 1 168 | 169 | You can also pass a file or directory. If the file/directory has an info hash in its name that will be used, 170 | or if it is a torrent file its info hash will be computed and used. If a directory is given it will be searched and 171 | each file in it will be looked up as if it were passed as an argument. 172 | 173 | $> gazelle-origin "./Pink Floyd The Wall.torrent" 174 | $> gazelle-origin ./899350BAF9F3671FE6E0817CBA7B9796E70DD924.torrent 175 | $> gazelle-origin ./torrents 176 | 177 | Using `-o file`, you can specify an output file: 178 | 179 | $> gazelle-origin -o origin.yaml 1 180 | 181 | Using `-p file`, you can specify a file to run after each output is saved. This program has access to information 182 | about the downloaded torrent and the output file through environment variables including OUT, ARTIST, NAME, DIRECTORY, EDITION, YEAR, FORMAT, ENCODING. 183 | 184 | $> gazelle-origin -o origin.yaml 1 -p ./post.sh 185 | 186 | You can use post scripts to populate your existing library with origin.yaml files using the info hashes of all your snatched torrents. 187 | If all of your torrents are in `./torrents/`, the corresponding data is in `/music/`, and you have a script `./script.sh` containing `mv $OUT "/music/$DIRECTORY/$OUT"`, 188 | then you could run 189 | 190 | $> gazelle-origin -o origin.yaml ./torrents -p ./script.sh 191 | 192 | Or you can manually go through your existing downloads and populate them with origin.yaml files: 193 | 194 | $> cd /path/to/first/torrent 195 | $> gazelle-origin -o origin.yaml "https://redacted.ch/torrents.php?torrentid=1" 196 | $> cd /path/to/another/torrent 197 | $> gazelle-origin -o origin.yaml "https://redacted.ch/torrents.php?torrentid=2" 198 | $> ... 199 | 200 | Integration 201 | ----------- 202 | 203 | ### Torrent clients 204 | 205 | `gazelle-origin` is best used when called automatically in your torrent client when a download finishes. Use the 206 | following snippets to integrate `gazelle-origin` into your client. If your client isn't listed, please file a PR! 207 | 208 | #### rtorrent 209 | 210 | `gazelle-origin` is best used when called automatically in your torrent client when 211 | a download finishes. For example, rTorrent users can add something like the 212 | following to their `~/.rtorrent.rc`: 213 | 214 | ~~~ 215 | method.set_key = event.download.finished,postrun,"execute2={sh,~/postdownload.sh,$d.base_path=,$d.hash=,$session.path=}" 216 | ~~~ 217 | 218 | Then, in `~/postdownload.sh`: 219 | ~~~ 220 | export RED_API_KEY= 221 | 222 | BASE_PATH=$1 223 | INFO_HASH=$2 224 | SESSION_PATH=$3 225 | if [[ $(grep flacsfor.me "$SESSION_PATH"/$INFO_HASH.torrent) ]]; then 226 | gazelle-origin -t red -o "$BASE_PATH"/origin.yaml $INFO_HASH 227 | fi 228 | ~~~ 229 | 230 | #### qBittorrent 231 | 232 | In Options > Downloads > Run an external program on torrent completion, enter the following: 233 | 234 | gazelle-origin -t %T -o "%R/origin.yaml" --api-key %I 235 | 236 | Note that this assumes Python has been added to your environment path. If not and you're a Windows user, you can 237 | fix this by enabling the checkbox at: 238 | _Start > Settings > Apps & Features > Python > Modify > Modify > Next > Add Python to environment variables_. 239 | 240 | ### beets 241 | 242 | Origin files can also be used by beets to significantly improve autotagger results. To do so, install the 243 | [beets-originquery](https://github.com/x1ppy/beets-originquery) plugin, using the following configuration: 244 | 245 | ~~~ 246 | originquery: 247 | origin_file: origin.yaml 248 | tag_patterns: 249 | media: '$.Media' 250 | year: '$."Edition year"' 251 | label: '$."Record label"' 252 | catalognum: '$."Catalog number"' 253 | albumdisambig: '$.Edition' 254 | ~~~ 255 | 256 | Changelog 257 | --------- 258 | ### [2.2.1] - 2020-11-16 259 | * Fix URL queries 260 | ### [2.2.0] - 2020-11-02 261 | * Add support for multiple inputs, files/directories, env file, and post scripts (thanks @a8f!) 262 | ### [2.1.1] - 2020-04-27 263 | * Accept any string containing "flacsfor.me" as RED tracker ID 264 | ### [2.1.0] - 2020-04-27 265 | * Added `--api-key` to allow specifying API key on execution 266 | * Accept "flacsfor.me" as a RED tracker ID 267 | * More sane package organization 268 | ### [2.0.4] - 2020-04-18 269 | * Fixed YAML generation for comments containing whitespace-only lines 270 | ### [2.0.3] - 2020-04-13 271 | * Replaced cookie with API key 272 | ### [2.0.2] - 2020-04-11 273 | * Added timeout for requests 274 | ### [2.0.1] - 2020-04-10 275 | * Fixed YAML generation bug for fields starting with quotes 276 | ### [2.0.0] - 2020-04-08 277 | * Renamed to `gazelle-origin` and switched to YAML output 278 | ### [1.0.0] - 2020-03-24 279 | * First tagged release 280 | 281 | [2.2.1]: https://github.com/x1ppy/gazelle-origin/compare/2.2.0...2.2.1 282 | [2.2.0]: https://github.com/x1ppy/gazelle-origin/compare/2.1.1...2.2.0 283 | [2.1.1]: https://github.com/x1ppy/gazelle-origin/compare/2.1.0...2.1.1 284 | [2.1.0]: https://github.com/x1ppy/gazelle-origin/compare/2.0.4...2.1.0 285 | [2.0.4]: https://github.com/x1ppy/gazelle-origin/compare/2.0.3...2.0.4 286 | [2.0.3]: https://github.com/x1ppy/gazelle-origin/compare/2.0.2...2.0.3 287 | [2.0.2]: https://github.com/x1ppy/gazelle-origin/compare/2.0.1...2.0.2 288 | [2.0.1]: https://github.com/x1ppy/gazelle-origin/compare/2.0.0...2.0.1 289 | [2.0.0]: https://github.com/x1ppy/gazelle-origin/compare/1.0.0...2.0.0 290 | [1.0.0]: https://github.com/x1ppy/gazelle-origin/releases/tag/1.0.0 291 | -------------------------------------------------------------------------------- /docs/api-checkboxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x1ppy/gazelle-origin/4bfffa575ace819b02d576e9f0b79d20335c03c5/docs/api-checkboxes.png -------------------------------------------------------------------------------- /docs/api-done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x1ppy/gazelle-origin/4bfffa575ace819b02d576e9f0b79d20335c03c5/docs/api-done.png -------------------------------------------------------------------------------- /gazelleorigin/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import GazelleAPI, GazelleAPIError 2 | -------------------------------------------------------------------------------- /gazelleorigin/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import io 4 | import os 5 | import re 6 | import subprocess 7 | import sys 8 | import yaml 9 | from hashlib import sha1 10 | from . import GazelleAPI, GazelleAPIError 11 | 12 | 13 | EXIT_CODES = { 14 | 'hash': 3, 15 | 'music': 4, 16 | 'unauthorized': 5, 17 | 'request': 6, 18 | 'request-json': 7, 19 | 'api-key': 8, 20 | 'tracker': 9, 21 | 'input-error': 10 22 | } 23 | 24 | parser = argparse.ArgumentParser( 25 | description='Fetches torrent origin information from Gazelle-based music trackers', 26 | formatter_class=argparse.RawDescriptionHelpFormatter, 27 | epilog='Either ORIGIN_TRACKER or --tracker must be set to a supported tracker:\n' 28 | ' redacted.ch: "RED", or any string containing "flacsfor.me"' 29 | ) 30 | parser.add_argument('torrent', nargs='+', help='torrent identifier, which can be either its info hash, torrent ID, permalink, or path to torrent file(s) whose name or computed info hash should be used') 31 | parser.add_argument('--out', '-o', help='Path to write origin data (default: print to stdout).', metavar='file') 32 | parser.add_argument('--tracker', '-t', metavar='tracker', 33 | help='Tracker to use. Optional if the ORIGIN_TRACKER environment variable is set.') 34 | parser.add_argument('--api-key', metavar='key', 35 | help='API key. Optional if the _API_KEY (e.g., RED_API_KEY) environment variable is set.') 36 | parser.add_argument('--env', '-e', nargs=1, metavar='file', help='file to load environment variables from') 37 | parser.add_argument('--post', '-p', nargs='+', metavar='file', default=[], help='script(s) to run after each output is written.\n' 38 | 'These scripts have access to environment variables with info about the item including OUT, ARTIST, NAME, DIRECTORY, EDITION, YEAR, FORMAT, ENCODING') 39 | parser.add_argument('--recursive', '-r', action='store_true', help='recursively search directories for files') 40 | parser.add_argument('--no-hash', '-n', action='store_true', help='don\'t compute hash from torrent files') 41 | parser.add_argument('--ignore-invalid', '-i', action='store_true', help='continue processing other arguments if an invalid id/hash is supplied') 42 | parser.add_argument('--deduplicate', '-d', action='store_true', help='if specified, only one torrent with any given id/hash will be fetched') 43 | 44 | 45 | api = None 46 | args = None 47 | fetched = {} 48 | environment = {} 49 | 50 | 51 | def main(): 52 | global api, args, environment 53 | 54 | args = parser.parse_args() 55 | for script in args.post: 56 | if not os.path.isfile(script): 57 | print('Invalid post script: ' + script) 58 | sys.exit(EXIT_CODES['input-error']) 59 | environment = {'out': args.out if args.out else 'stdout'} 60 | 61 | if args.env: 62 | try: 63 | with open(args.env[0], 'r') as envfile: 64 | for line in envfile.readlines(): 65 | var = line.rstrip().split('=', 1) 66 | if len(var) != 2: 67 | if len(var) != 0: 68 | print('Skipping invalid line in env file: ' + line) 69 | continue 70 | if var[0] == 'RED_API_KEY': 71 | environment['api_key'] = var[1] 72 | elif var[0] == 'ORIGIN_TRACKER': 73 | environment['tracker'] = var[1] 74 | else: 75 | environment[var[0]] = var[1] 76 | except IOError: 77 | print('Unable to open file ' + args.env[0]) 78 | sys.exit(EXIT_CODES['input-error']) 79 | 80 | if args.api_key: 81 | environment['api_key'] = args.api_key 82 | elif os.environ.get('RED_API_KEY'): 83 | environment['api_key'] = os.environ.get('RED_API_KEY') 84 | 85 | if not environment['api_key']: 86 | print('API key must be provided using either --api-key or setting the _API_KEY environment variable.', file=sys.stderr) 87 | sys.exit(EXIT_CODES['api-key']) 88 | 89 | 90 | if args.tracker: 91 | environment['tracker'] = args.tracker 92 | elif os.environ.get('ORIGIN_TRACKER'): 93 | environment['tracker'] = os.environ.get('ORIGIN_TRACKER') 94 | 95 | if not environment['tracker']: 96 | print('Tracker must be provided using either --tracker or setting the ORIGIN_TRACKER environment variable.', 97 | file=sys.stderr) 98 | sys.exit(EXIT_CODES['tracker']) 99 | if environment['tracker'].lower() != 'red' and 'flacsfor.me' not in environment['tracker'].lower(): 100 | print('Invalid tracker: {0}'.format(environment['tracker']), file=sys.stderr) 101 | sys.exit(EXIT_CODES['tracker']) 102 | 103 | try: 104 | api = GazelleAPI(environment['api_key']) 105 | except GazelleAPIError as e: 106 | print('Error initializing Gazelle API client') 107 | sys.exit(EXIT_CODES[e.code]) 108 | 109 | for arg in args.torrent: 110 | handle_input_torrent(arg, True, args.recursive) 111 | 112 | 113 | """ 114 | Parse hash or id of torrent 115 | torrent can be an id, hash, url, or path 116 | """ 117 | def parse_torrent_input(torrent, walk=True, recursive=False): 118 | # torrent is literal infohash 119 | if re.match(r'^[\da-fA-F]{40}$', torrent): 120 | return {'hash': torrent} 121 | # torrent is literal id 122 | if re.match(r'^\d+$', torrent): 123 | return {'id': torrent} 124 | # torrent is valid path 125 | if os.path.exists(torrent): 126 | if walk and os.path.isdir(torrent): 127 | for path in map(lambda x: os.path.join(torrent, x), os.listdir(torrent)): 128 | handle_input_torrent(path, recursive, recursive) 129 | return 'walked' 130 | # If file/dir name is info hash use that 131 | filename = os.path.split(torrent)[-1].split('.')[0] 132 | if re.match(r'^[\da-fA-F]{40}$', filename): 133 | return {'hash': filename} 134 | # If torrent file compute the info hash 135 | if not args.no_hash and os.path.isfile(torrent) and os.path.split(torrent)[-1].endswith('.torrent'): 136 | global encode, decode 137 | if 'encode' not in globals() or 'decode' not in globals(): 138 | try: 139 | from bencoder import encode, decode 140 | except: 141 | print('Found torrent file ' + torrent + ' but unable to load bencoder module to compute hash') 142 | print('Install bencoder (pip install bencoder) then try again or pass --no-hash to not compute the hash') 143 | if args.ignore_invalid: 144 | return None 145 | else: 146 | sys.exit(EXIT_CODES['input-error']) 147 | with open(torrent, 'rb') as torrent: 148 | try: 149 | decoded = decode(torrent.read()) 150 | info_hash = sha1(encode(decoded[b'info'])).hexdigest() 151 | except: 152 | return None 153 | return {'hash': info_hash} 154 | # torrent is a URL 155 | url_match = re.match(r'.*torrentid=(\d+).*', torrent) 156 | if not url_match or url_match.lastindex < 1: 157 | return None 158 | return {'id': url_match[1]} 159 | 160 | 161 | """ 162 | Get torrent's info from GazelleAPI 163 | torrent can be an id, hash, url, or path 164 | """ 165 | def handle_input_torrent(torrent, walk=True, recursive=False): 166 | parsed = parse_torrent_input(torrent, walk, recursive) 167 | if parsed == 'walked': 168 | return 169 | if not parsed: 170 | print('Invalid torrent ID, hash, file, or URL: ' + torrent, file=sys.stderr) 171 | if args.ignore_invalid: 172 | return 173 | sys.exit(EXIT_CODES['hash']) 174 | 175 | if args.deduplicate: 176 | if 'id' in parsed: 177 | if parsed['id'] in fetched: 178 | return 179 | fetched[parsed['id']] = True 180 | if 'hash' in parsed: 181 | if parsed['hash'] in fetched: 182 | return 183 | fetched[parsed['hash']] = True 184 | 185 | # Actually get the info from the API 186 | try: 187 | info = api.get_torrent_info(**parsed) 188 | except GazelleAPIError as e: 189 | if not args.ignore_invalid: 190 | skip = False 191 | elif e.code == 'request': 192 | # If server returned 500 series error then stop because server might be having trouble 193 | skip = int(str(e).split('(status ')[-1][:-1]) >= 500 194 | else: 195 | skip = e.code == 'request-json' or e.code == 'music' 196 | if skip: 197 | print('Got %s retrieving %s, skipping' % (str(e), torrent)) 198 | return 199 | else: 200 | print(e, file=sys.stderr) 201 | sys.exit(EXIT_CODES[e.code]) 202 | 203 | if args.out: 204 | with io.open(args.out, 'a' if os.path.exists(args.out) else 'w', encoding='utf-8') as f: 205 | f.write(info) 206 | else: 207 | print(info, end='') 208 | 209 | if args.post: 210 | fetched_info = yaml.load(info, Loader=yaml.SafeLoader) 211 | for script in args.post: 212 | subprocess.run(script, shell=True, env={k.upper(): str(v) for k, v in {**environment, **fetched_info}.items()}) 213 | 214 | if __name__ == '__main__': 215 | main() 216 | -------------------------------------------------------------------------------- /gazelleorigin/core.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | import requests 4 | import textwrap 5 | import yaml 6 | 7 | 8 | headers = { 9 | 'Connection': 'keep-alive', 10 | 'Cache-Control': 'max-age=0', 11 | 'User-Agent': 'gazelle-origin', 12 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 13 | 'Accept-Encoding': 'gzip,deflate,sdch', 14 | 'Accept-Language': 'en-US,en;q=0.8', 15 | 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3'} 16 | 17 | 18 | class GazelleAPIError(Exception): 19 | def __init__(self, code, message): 20 | super().__init__() 21 | self.code = code 22 | self.message = message 23 | 24 | def __str__(self): 25 | return self.message 26 | 27 | 28 | # GazelleAPI code is based off of REDbetter (https://github.com/Mechazawa/REDBetter-crawler). 29 | class GazelleAPI: 30 | def __init__(self, api_key): 31 | self.session = requests.Session() 32 | self.session.headers.update(headers) 33 | self.session.headers.update({'Authorization': api_key}) 34 | 35 | def request(self, action, **kwargs): 36 | ajaxpage = 'https://redacted.ch/ajax.php' 37 | params = {'action': action} 38 | params.update(kwargs) 39 | 40 | r = self.session.get(ajaxpage, params=params, allow_redirects=False, timeout=30) 41 | if r.status_code == 401 or r.status_code == 403: 42 | raise GazelleAPIError('unauthorized', 'Authentication error: ' + r.json()['error']) 43 | if r.status_code != 200: 44 | raise GazelleAPIError('request', 45 | 'Could not retrieve origin data. Try again later. (status {0})'.format(r.status_code)) 46 | 47 | parsed = json.loads(r.content) 48 | if parsed['status'] != 'success': 49 | raise GazelleAPIError('request-json', 'Could not retrieve origin data. Check the torrent ID/hash or try again later.') 50 | 51 | return parsed['response'] 52 | 53 | def _make_table(self, dict): 54 | k_width = max(len(html.unescape(k)) for k in dict.keys()) + 2 55 | result = '' 56 | for k,v in dict.items(): 57 | if v == "''": 58 | v = '~' 59 | result += "".join((html.unescape((k + ':').ljust(k_width)), v)) + '\n' 60 | return result 61 | 62 | def get_torrent_info(self, hash=None, id=None): 63 | info = self.request('torrent', hash=hash, id=id) 64 | group = info['group'] 65 | torrent = info['torrent'] 66 | 67 | if group['categoryName'] != 'Music': 68 | raise GazelleAPIError('music', 'Not a music torrent') 69 | 70 | artists = group['musicInfo']['artists'] 71 | if len(artists) == 1: 72 | artists = artists[0]['name'] 73 | elif len(artists) == 2: 74 | artists = '{0} & {1}'.format(artists[0]['name'], artists[1]['name']) 75 | else: 76 | artists = 'Various Artists' 77 | 78 | dict = {k:html.unescape(v) if isinstance(v, str) else v for k,v in { 79 | 'Artist': artists, 80 | 'Name': group['name'], 81 | 'Edition': torrent['remasterTitle'], 82 | 'Edition year': torrent['remasterYear'] or '', 83 | 'Media': torrent['media'], 84 | 'Catalog number': torrent['remasterCatalogueNumber'], 85 | 'Record label': torrent['remasterRecordLabel'], 86 | 'Original year': group['year'] or '', 87 | 'Format': torrent['format'], 88 | 'Encoding': torrent['encoding'], 89 | 'Log': '{0}%'.format(torrent['logScore']) if torrent['hasLog'] else '', 90 | 'Directory': torrent['filePath'], 91 | 'Size': torrent['size'], 92 | 'File count': torrent['fileCount'], 93 | 'Info hash': torrent['infoHash'], 94 | 'Uploaded': torrent['time'], 95 | 'Permalink': 'https://redacted.ch/torrents.php?torrentid={0}'.format(torrent['id']), 96 | }.items()} 97 | 98 | dump = yaml.dump(dict, width=float('inf'), sort_keys=False, allow_unicode=True) 99 | 100 | out = {} 101 | for line in dump.strip().split('\n'): 102 | key, value = line.split(':', 1) 103 | if key == 'Uploaded' or key == 'Encoding': 104 | value = value.replace("'", '') 105 | out[key] = value.strip() 106 | 107 | result = self._make_table(out) + '\n' 108 | 109 | comment = html.unescape(torrent['description']).strip('\r\n') 110 | if comment: 111 | comment = textwrap.indent(comment, ' ', lambda line: True) 112 | result += 'Comment: |-\n{0}\n\n'.format(comment) 113 | 114 | out = [] 115 | for el in html.unescape(torrent['fileList']).replace('}}}', '').split('|||'): 116 | name, size = el.split('{{{') 117 | out.append({'Name': name, 'Size': int(size)}) 118 | result += yaml.dump({'Files': out}, width=float('inf'), allow_unicode=True) 119 | 120 | return result 121 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="gazelle-origin", 8 | version="2.2.1", 9 | author="x1ppy", 10 | author_email="", 11 | packages=[ 12 | 'gazelleorigin', 13 | ], 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'gazelle-origin = gazelleorigin.__main__:main', 17 | ], 18 | }, 19 | description="Gazelle origin.yaml generator", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | url="https://github.com/x1ppy/gazelle-origin", 23 | python_requires='>=3.5.2', 24 | install_requires=[ 25 | "pyyaml", 26 | "requests", 27 | ], 28 | ) 29 | --------------------------------------------------------------------------------