├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── GazelleUI.py ├── assets ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── logo.png ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png ├── normalize.css ├── script.js ├── skeleton.css ├── spinner.svg └── style.css ├── config └── .gitignore ├── lib ├── __init__.py ├── auth.py ├── autofetch.py ├── database.py ├── discord.py ├── jobs.py ├── settings.py ├── torrent.py ├── wat.py └── whatapi │ ├── __init__.py │ └── whatapi.py ├── readme.md ├── requirements.txt ├── templates ├── _snatch_table.html ├── artist.html ├── browse.html ├── group_info.html ├── index.html ├── layout.html ├── settings.html ├── shared │ └── _filters.html ├── snatches.html └── subscriptions.html ├── torrents └── .keep └── wsgi.py /.dockerignore: -------------------------------------------------------------------------------- 1 | config/data.sqlite3 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Docker Hub 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Build and push Docker image 22 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 23 | with: 24 | context: . 25 | push: true 26 | tags: xanderstrike/gazelleui:latest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | *.torrent 3 | *.pyc 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python 2 | VOLUME ["/torrents", "/app/config"] 3 | COPY requirements.txt /app/requirements.txt 4 | RUN pip install -r /app/requirements.txt 5 | COPY . /app 6 | WORKDIR /app 7 | ENTRYPOINT ["python"] 8 | CMD ["GazelleUI.py"] 9 | -------------------------------------------------------------------------------- /GazelleUI.py: -------------------------------------------------------------------------------- 1 | # WhatUI 2 | # https://github.com/XanderStrike/WhatUI 3 | 4 | import os 5 | import sys 6 | 7 | from flask import Flask, request, jsonify, render_template, redirect, send_from_directory, Response 8 | from flask_apscheduler import APScheduler 9 | 10 | from lib.auth import requires_auth 11 | import lib.database as database 12 | import lib.settings as settings 13 | import lib.wat as wat 14 | import lib.jobs as jobs 15 | import lib.torrent as torrent 16 | import lib.autofetch as autofetch 17 | 18 | import json 19 | 20 | # import logging 21 | # logging.basicConfig() 22 | 23 | # Configure Scheduler 24 | class Config(object): 25 | JOBS = jobs.job_list() 26 | SCHEDULER_VIEWS_ENABLED = True 27 | SCHEDULER_TIMEZONE = "America/Los_Angeles" 28 | DEBUG = True 29 | 30 | app = Flask(__name__) 31 | app.config.from_object(Config()) 32 | 33 | # if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': 34 | scheduler = APScheduler() 35 | scheduler.init_app(app) 36 | scheduler.start() 37 | 38 | 39 | # Initialize Database 40 | database.init() 41 | 42 | 43 | # Routes 44 | @app.route("/") 45 | @requires_auth 46 | def index(): 47 | setting = settings.get('what_credentials') 48 | if setting[1] == None or setting[1] == '': 49 | return render_template('settings.html', settings=settings.get_all(), message="Please set your whatcd username and password.", message_class="alert-error") 50 | torrents = torrent.get_recent() 51 | return render_template('index.html', torrents=torrents, userinfo=database.userinfo()) 52 | 53 | @app.route("/artist") 54 | @requires_auth 55 | def artist(): 56 | query = request.args['q'] 57 | results = wat.get_artist(query) 58 | return render_template('artist.html', results=results, userinfo=database.userinfo()) 59 | 60 | @app.route("/browse") 61 | @requires_auth 62 | def browse(): 63 | query = request.args['q'] 64 | results = wat.browse(query) 65 | return render_template('browse.html', results=results, query=query, userinfo=database.userinfo()) 66 | 67 | @app.route("/label") 68 | @requires_auth 69 | def label(): 70 | query = request.args['q'] 71 | results = wat.label(query) 72 | return render_template('browse.html', results=results, query=query, userinfo=database.userinfo()) 73 | 74 | @app.route("/want", methods=['POST']) 75 | @requires_auth 76 | def want(): 77 | torrent.queue(json.loads(request.form['data'])) 78 | return "" 79 | 80 | @app.route("/group_info") 81 | @requires_auth 82 | def group_info(): 83 | group_id = request.args['id'] 84 | results = wat.get_group(group_id) 85 | return render_template('group_info.html', group_info=results) 86 | 87 | @app.route("/settings", methods=['GET', 'POST']) 88 | @requires_auth 89 | def settings_path(): 90 | output = {'message':None,'class':None} 91 | if request.method == 'POST': 92 | output = settings.update(request.form) 93 | wat.bust_handle_cache() 94 | return render_template('settings.html', settings=settings.get_all(), message=output['message'], message_class=output['class'], userinfo=database.userinfo()) 95 | 96 | @app.route("/snatches") 97 | @requires_auth 98 | def snatches(): 99 | torrents = torrent.get_all() 100 | return render_template('snatches.html', torrents=torrents, userinfo=database.userinfo()) 101 | 102 | @app.route('/delete_sub/') 103 | @requires_auth 104 | def delete_sub(sub_id): 105 | database.delete_sub(sub_id) 106 | return redirect('/subscriptions') 107 | 108 | @app.route('/create_sub', methods=['POST']) 109 | @requires_auth 110 | def create_sub(): 111 | autofetch.create_subscription(request.form) 112 | return redirect('/subscriptions') 113 | 114 | @app.route("/subscriptions") 115 | @requires_auth 116 | def subscriptions(): 117 | subs = database.subscriptions() 118 | return render_template('subscriptions.html', subs=subs, userinfo=database.userinfo()) 119 | 120 | # Serve Static Assets 121 | @app.route('/assets/') 122 | def send_assets(filename): 123 | return send_from_directory('assets', filename) 124 | 125 | @app.route('/') 126 | def catch_all(filename): 127 | return send_from_directory('assets', filename) 128 | 129 | # It's Go Time 130 | if __name__ == "__main__": 131 | network_settings = settings.get('network') 132 | app.run(host=network_settings[1], port=int(network_settings[2]), use_reloader=False, debug=False) 133 | -------------------------------------------------------------------------------- /assets/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-144x144.png -------------------------------------------------------------------------------- /assets/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-192x192.png -------------------------------------------------------------------------------- /assets/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-36x36.png -------------------------------------------------------------------------------- /assets/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-48x48.png -------------------------------------------------------------------------------- /assets/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-72x72.png -------------------------------------------------------------------------------- /assets/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/android-icon-96x96.png -------------------------------------------------------------------------------- /assets/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-114x114.png -------------------------------------------------------------------------------- /assets/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-120x120.png -------------------------------------------------------------------------------- /assets/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-144x144.png -------------------------------------------------------------------------------- /assets/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-152x152.png -------------------------------------------------------------------------------- /assets/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-180x180.png -------------------------------------------------------------------------------- /assets/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-57x57.png -------------------------------------------------------------------------------- /assets/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-60x60.png -------------------------------------------------------------------------------- /assets/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-72x72.png -------------------------------------------------------------------------------- /assets/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-76x76.png -------------------------------------------------------------------------------- /assets/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon-precomposed.png -------------------------------------------------------------------------------- /assets/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/apple-icon.png -------------------------------------------------------------------------------- /assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/favicon-96x96.png -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/favicon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/logo.png -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GazelleUI", 3 | "start_url": "/", 4 | "display": "standalone", 5 | "icons": [ 6 | { 7 | "src": "\/android-icon-36x36.png", 8 | "sizes": "36x36", 9 | "type": "image\/png", 10 | "density": "0.75" 11 | }, 12 | { 13 | "src": "\/android-icon-48x48.png", 14 | "sizes": "48x48", 15 | "type": "image\/png", 16 | "density": "1.0" 17 | }, 18 | { 19 | "src": "\/android-icon-72x72.png", 20 | "sizes": "72x72", 21 | "type": "image\/png", 22 | "density": "1.5" 23 | }, 24 | { 25 | "src": "\/android-icon-96x96.png", 26 | "sizes": "96x96", 27 | "type": "image\/png", 28 | "density": "2.0" 29 | }, 30 | { 31 | "src": "\/android-icon-144x144.png", 32 | "sizes": "144x144", 33 | "type": "image\/png", 34 | "density": "3.0" 35 | }, 36 | { 37 | "src": "\/android-icon-192x192.png", 38 | "sizes": "192x192", 39 | "type": "image\/png", 40 | "density": "4.0" 41 | }, 42 | ] 43 | } -------------------------------------------------------------------------------- /assets/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/ms-icon-144x144.png -------------------------------------------------------------------------------- /assets/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/ms-icon-150x150.png -------------------------------------------------------------------------------- /assets/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/ms-icon-310x310.png -------------------------------------------------------------------------------- /assets/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/assets/ms-icon-70x70.png -------------------------------------------------------------------------------- /assets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /assets/script.js: -------------------------------------------------------------------------------- 1 | // Search 2 | document.querySelector('.js-more-search')?.addEventListener('click', function() { 3 | const advSearch = document.querySelector('.js-advanced-search'); 4 | advSearch.style.display = advSearch.style.display === 'none' ? 'block' : 'none'; 5 | }); 6 | 7 | document.querySelector('.js-search')?.addEventListener('submit', function() { 8 | document.querySelector('.js-search-box').style.display = 'none'; 9 | document.querySelector('.js-body-content').style.display = 'none'; 10 | document.querySelector('.js-search-loading').style.display = 'block'; 11 | }); 12 | 13 | // Release Filters 14 | function do_filters() { 15 | const regex = new RegExp(document.querySelector('.js-text-filter')?.value || '', "i"); 16 | const rows = document.querySelectorAll('.js-release'); 17 | const type = document.querySelector('.js-type-filter')?.value; 18 | 19 | rows.forEach(row => { 20 | const title = row.querySelector(".js-title").innerHTML; 21 | if (title.search(regex) !== -1 && (type === '0' || type === undefined || row.classList.contains("js-release-type-" + type))) { 22 | row.style.display = ''; 23 | } else { 24 | row.style.display = 'none'; 25 | } 26 | }); 27 | } 28 | 29 | document.querySelector('.js-type-filter')?.addEventListener('change', do_filters); 30 | document.querySelector('.js-text-filter')?.addEventListener('keyup', do_filters); 31 | document.addEventListener('DOMContentLoaded', do_filters); 32 | 33 | // Fetch 34 | document.querySelectorAll('.js-download').forEach(elem => { 35 | elem.addEventListener('click', function(e) { 36 | e.preventDefault(); 37 | fetch(this.getAttribute('href'), { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/x-www-form-urlencoded', 41 | }, 42 | body: 'data=' + encodeURIComponent(this.getAttribute('torrentInfo')) 43 | }) 44 | .then(response => response.text()) 45 | .then(data => { 46 | this.outerHTML = data; 47 | }); 48 | return false; 49 | }); 50 | }); 51 | 52 | // Artist Info 53 | document.querySelector('.js-desc-more-link')?.addEventListener('click', function(e) { 54 | e.preventDefault(); 55 | document.querySelector('.js-artist-desc').style.maxHeight = 'none'; 56 | document.querySelector('.js-desc-less-link').style.display = 'block'; 57 | this.style.display = 'none'; 58 | }); 59 | 60 | document.querySelector('.js-desc-less-link')?.addEventListener('click', function(e) { 61 | e.preventDefault(); 62 | document.querySelector('.js-artist-desc').style.maxHeight = '190px'; 63 | document.querySelector('.js-desc-more-link').style.display = 'block'; 64 | this.style.display = 'none'; 65 | }); 66 | 67 | // Expand Torrents 68 | document.querySelectorAll('.js-torrent-more-link').forEach(elem => { 69 | elem.addEventListener('click', function(e) { 70 | e.preventDefault(); 71 | this.style.display = 'none'; 72 | document.querySelector('.js-torrent-container-' + this.getAttribute('groupId')).style.maxHeight = 'none'; 73 | }); 74 | }); 75 | 76 | // Release Info 77 | document.querySelectorAll('.js-more-info').forEach(elem => { 78 | elem.addEventListener('click', function(e) { 79 | e.preventDefault(); 80 | const groupId = this.getAttribute('groupId'); 81 | const infoContainer = document.querySelector('.js-more-info-' + groupId); 82 | 83 | if (!infoContainer.innerHTML.trim()) { 84 | fetch(this.getAttribute('href')) 85 | .then(response => response.text()) 86 | .then(data => { 87 | infoContainer.innerHTML = data; 88 | }); 89 | } 90 | 91 | this.style.display = 'none'; 92 | infoContainer.style.display = 'block'; 93 | document.querySelector(`.js-less-info[groupId="${groupId}"]`).style.display = 'block'; 94 | }); 95 | }); 96 | 97 | document.querySelectorAll('.js-less-info').forEach(elem => { 98 | elem.addEventListener('click', function(e) { 99 | e.preventDefault(); 100 | const groupId = this.getAttribute('groupId'); 101 | this.style.display = 'none'; 102 | document.querySelector(`.js-more-info[groupId="${groupId}"]`).style.display = 'block'; 103 | document.querySelector('.js-more-info-' + groupId).style.display = 'none'; 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /assets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /assets/spinner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top:30px; 3 | background-color: #461e1e; 4 | color: #fff; 5 | } 6 | 7 | h1 { 8 | margin-bottom: 0; 9 | } 10 | 11 | a { 12 | text-decoration: underline; 13 | color: #fff; 14 | } 15 | 16 | a:hover { 17 | color: #fff; 18 | } 19 | 20 | input, select { 21 | color: #000; 22 | } 23 | 24 | input[type="submit"], input[type="submit"]:hover { 25 | color: #fff; 26 | } 27 | 28 | .small-up-column { 29 | display: none; 30 | } 31 | 32 | .medium-up-column { 33 | display: none; 34 | } 35 | 36 | @media (min-width: 550px) { 37 | .small-up-column { 38 | display: table-cell; 39 | } 40 | } 41 | 42 | @media (min-width: 750px) { 43 | .medium-up-column { 44 | display: table-cell; 45 | } 46 | } 47 | 48 | .u-hidden { 49 | display: none; 50 | } 51 | 52 | table { 53 | max-width: 100%; 54 | } 55 | 56 | .artist-desc { 57 | max-height: 190px; 58 | overflow: hidden; 59 | } 60 | 61 | .desc-more { 62 | text-align: center; 63 | margin-bottom: 20px; 64 | } 65 | 66 | .torrent-container { 67 | max-height: 275px; 68 | overflow: hidden; 69 | } 70 | 71 | .brand-link { 72 | margin-top:50px; 73 | text-decoration:none; 74 | color: #fff; 75 | } 76 | 77 | .brand-link:hover { 78 | color: #fff; 79 | } 80 | 81 | .settings-link { 82 | float:right; 83 | text-decoration:none; 84 | color:#000; 85 | } 86 | 87 | .more-search { 88 | text-align: center; 89 | cursor: pointer; 90 | } 91 | 92 | .advanced-search { 93 | display: none; 94 | } 95 | 96 | .search-loading { 97 | text-align: center; 98 | display: none; 99 | } 100 | 101 | /* alerts */ 102 | .alert { 103 | display: block; 104 | padding: 20px; 105 | border-left: 5px solid; 106 | } 107 | 108 | .alert-success { 109 | background-color: #D5F5E3; 110 | border-left-color: #2ECC71; 111 | color: #2ECC71; 112 | } 113 | 114 | .alert-info { 115 | background-color: #D6EAF8; 116 | border-left-color: #3498DB; 117 | color: #3498DB; 118 | } 119 | 120 | .alert-warning { 121 | background-color: #FCF3CF; 122 | border-left-color: #F1C40F; 123 | color: #F1C40F; 124 | } 125 | 126 | .alert-error { 127 | background-color: #F2D7D5; 128 | border-left-color: #C0392B; 129 | color: #C0392B; 130 | } 131 | 132 | .button, .button:hover { 133 | color: #fff; 134 | } 135 | 136 | .button:disabled { 137 | border: 1px solid #E3E3E3; 138 | color: #888; 139 | cursor: not-allowed; 140 | } 141 | 142 | 143 | .loader, 144 | .loader:before, 145 | .loader:after { 146 | border-radius: 50%; 147 | width: 2.5em; 148 | height: 2.5em; 149 | -webkit-animation-fill-mode: both; 150 | animation-fill-mode: both; 151 | -webkit-animation: load7 1.8s infinite ease-in-out; 152 | animation: load7 1.8s infinite ease-in-out; 153 | } 154 | .loader { 155 | color: #ffffff; 156 | font-size: 10px; 157 | margin: 80px auto; 158 | position: relative; 159 | text-indent: -9999em; 160 | -webkit-transform: translateZ(0); 161 | -ms-transform: translateZ(0); 162 | transform: translateZ(0); 163 | -webkit-animation-delay: -0.16s; 164 | animation-delay: -0.16s; 165 | } 166 | .loader:before, 167 | .loader:after { 168 | content: ''; 169 | position: absolute; 170 | top: 0; 171 | } 172 | .loader:before { 173 | left: -3.5em; 174 | -webkit-animation-delay: -0.32s; 175 | animation-delay: -0.32s; 176 | } 177 | .loader:after { 178 | left: 3.5em; 179 | } 180 | @-webkit-keyframes load7 { 181 | 0%, 182 | 80%, 183 | 100% { 184 | box-shadow: 0 2.5em 0 -1.3em; 185 | } 186 | 40% { 187 | box-shadow: 0 2.5em 0 0; 188 | } 189 | } 190 | @keyframes load7 { 191 | 0%, 192 | 80%, 193 | 100% { 194 | box-shadow: 0 2.5em 0 -1.3em; 195 | } 196 | 40% { 197 | box-shadow: 0 2.5em 0 0; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/config/.gitignore -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/lib/__init__.py -------------------------------------------------------------------------------- /lib/auth.py: -------------------------------------------------------------------------------- 1 | # Original: http://flask.pocoo.org/snippets/8/ 2 | 3 | from flask import request, Response 4 | from functools import wraps 5 | import lib.settings as settings 6 | 7 | def check_auth(username, password): 8 | creds = settings.get('webui_credentials') 9 | return username == creds[1] and password == creds[2] 10 | 11 | def authenticate(): 12 | return Response( 13 | 'Could not verify your access level for that URL.\n' 14 | 'You have to login with proper credentials', 401, 15 | {'WWW-Authenticate': 'Basic realm="Login Required"'}) 16 | 17 | def requires_auth(f): 18 | @wraps(f) 19 | def decorated(*args, **kwargs): 20 | auth = request.authorization 21 | if not needs_auth() and (not auth or not check_auth(auth.username, auth.password)): 22 | return authenticate() 23 | return f(*args, **kwargs) 24 | return decorated 25 | 26 | def needs_auth(): 27 | creds = settings.get('webui_credentials') 28 | return creds[1] == None or creds[1] == '' 29 | -------------------------------------------------------------------------------- /lib/autofetch.py: -------------------------------------------------------------------------------- 1 | from . import database 2 | from . import torrent 3 | from . import wat 4 | 5 | def word_to_type(release_type): 6 | mapping = { 7 | 'Album': 1, 8 | 'Soundtrack': 3, 9 | 'EP': 5, 10 | 'Anthology': 6, 11 | 'Compilation': 7, 12 | 'Single': 9, 13 | 'Live album': 11, 14 | 'Remix': 13, 15 | 'Bootleg': 14, 16 | 'Interview': 15, 17 | 'Mixtape': 16, 18 | 'Unknown': 21, 19 | 'Demo': 23 20 | } 21 | 22 | return mapping[release_type] 23 | 24 | def create_subscription(data): 25 | query = 'insert into subscriptions(search_type, term, quality, release_type) values (' + \ 26 | '"' + data['search_type'] + '", ' + \ 27 | '"' + data['term'] + '", ' + \ 28 | '"' + data['quality'] + '", ' + \ 29 | '"' + data['release_type'] + '")' 30 | 31 | database.update(query) 32 | 33 | def enqueue(data): 34 | if not torrent.exists(data['id'], data['artist'], data['album']): 35 | print("Downloading") 36 | torrent.queue(data) 37 | else: 38 | print("Skipping, already downloaded") 39 | 40 | def fetch_new_torrents(sub): 41 | print(sub['search_type'] + ' search for "' + sub['term'] + \ 42 | '" with quality ' + sub['quality'] + ' and release type ' + str(sub['release_type'])) 43 | 44 | if sub['search_type'] == 'artist': 45 | data = wat.get_artist(sub['term']) 46 | 47 | if data == 'no data': 48 | print('Nothing found') 49 | return 50 | 51 | for group in data['torrentgroup']: 52 | if int(group['releaseType']) == int(sub['release_type']): 53 | for t in group['torrent']: 54 | if t['encoding'] == sub['quality']: 55 | print("Found " + group['groupName'] + ' (' + str(t['id']) + ')') 56 | t.update({ 57 | 'artist': data['name'], 58 | 'album': group['groupName'] 59 | }) 60 | enqueue(t) 61 | break 62 | elif sub['search_type'] == 'label': 63 | data = wat.label(sub['term']) 64 | 65 | for res in data['results']: 66 | if word_to_type(res['releaseType']) == int(sub['release_type']): 67 | for t in res['torrents']: 68 | if t['encoding'] == sub['quality']: 69 | print('Found ' + res['groupName']) 70 | t.update({ 71 | 'artist': res['artist'], 72 | 'album': res['groupName'] 73 | }) 74 | enqueue(t) 75 | break 76 | 77 | def run(): 78 | for sub in database.subscriptions(): 79 | fetch_new_torrents(sub) 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /lib/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 as lite 2 | 3 | SCHEMA = { 4 | 'settings': 5 | [ 6 | "key PRIMARY KEY", 7 | "value_1", 8 | "value_2" 9 | ], 10 | 'torrents': 11 | [ 12 | 'id PRIMARY KEY', 13 | 'artist', 14 | 'album', 15 | 'release', 16 | 'quality', 17 | 'added DATETIME', 18 | 'downloaded BOOLEAN' 19 | ], 20 | 'user': 21 | [ 22 | 'username PRIMARY KEY', 23 | 'upload', 24 | 'download', 25 | 'ratio', 26 | 'requiredratio', 27 | 'class', 28 | 'notifications', 29 | 'newSubscriptions', 30 | 'messages', 31 | 'newBlog' 32 | ], 33 | 'subscriptions': 34 | [ 35 | 'id INTEGER PRIMARY KEY AUTOINCREMENT', 36 | 'search_type', 37 | 'term', 38 | 'quality', 39 | 'release_type' 40 | ] 41 | } 42 | 43 | DEFAULT_SETTINGS = [ 44 | ['what_credentials', '', ''], 45 | ['webui_credentials', '', ''], 46 | ['network', '0.0.0.0', '2020'], 47 | ['torrent', '/torrents/', ''], 48 | ['domain', 'https://redacted.sh', ''], 49 | ['discord', '', ''] 50 | ] 51 | 52 | DB = 'config/data.sqlite3' 53 | 54 | def init(): 55 | con = lite.connect(DB) 56 | 57 | for k in list(SCHEMA.keys()): 58 | con.cursor().execute("create table if not exists " + k + "(" + ", ".join(SCHEMA[k]) + ");") 59 | 60 | for setting in DEFAULT_SETTINGS: 61 | con.cursor().execute("insert into settings(key, value_1, value_2) select '" + "', '".join(setting) + "' where not exists(select 1 from settings where key = '" + setting[0] + "')") 62 | 63 | con.commit() 64 | 65 | if (con.cursor().execute("select count(1) from user").fetchall() == [(0,)]): 66 | con.cursor().execute("insert into user(username) select ''") 67 | 68 | con.commit() 69 | 70 | def update(query): 71 | con = lite.connect(DB) 72 | con.cursor().execute(query) 73 | con.commit() 74 | return True 75 | 76 | def fetch(query): 77 | cur = lite.connect(DB).cursor() 78 | return cur.execute(query).fetchall() 79 | 80 | def row_fetch(query): 81 | con = lite.connect(DB) 82 | con.row_factory = lite.Row 83 | return con.cursor().execute(query).fetchall() 84 | 85 | def userinfo(): 86 | return fetch('select * from user')[0] 87 | 88 | def subscriptions(): 89 | res = fetch('select search_type, term, quality, release_type, id from subscriptions') 90 | h = [] 91 | for r in res: 92 | h.append({ 93 | 'search_type': r[0], 94 | 'term': r[1], 95 | 'quality': r[2], 96 | 'release_type': r[3], 97 | 'id': r[4] 98 | }) 99 | 100 | return h 101 | 102 | def delete_sub(id): 103 | update("delete from subscriptions where id = " + str(id)) 104 | -------------------------------------------------------------------------------- /lib/discord.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from . import settings as settings 3 | 4 | def send(message): 5 | url = settings.get('discord')[1] 6 | if url: 7 | requests.post(url, data = {"content":message, "username":"GazelleUI", "avatar_url":"https://xanderstrike.com/gazelleui.png"}) 8 | -------------------------------------------------------------------------------- /lib/jobs.py: -------------------------------------------------------------------------------- 1 | from . import torrent as torrent 2 | from . import wat as wat 3 | from . import autofetch as af 4 | 5 | def job_list(): 6 | return [ 7 | { 8 | 'id': 'torrents', 9 | 'func': '__main__:jobs.download_torrents', 10 | 'trigger': 'interval', 11 | 'seconds': 10 12 | }, 13 | { 14 | 'id': 'update_user_info', 15 | 'func': '__main__:jobs.update_user', 16 | 'trigger': 'interval', 17 | 'seconds': 900 18 | }, 19 | { 20 | 'id': 'autofetch', 21 | 'func': '__main__:jobs.autofetch', 22 | 'trigger': 'interval', 23 | 'seconds': 11520 24 | } 25 | ] 26 | 27 | def download_torrents(): 28 | torrent.download_all() 29 | 30 | def update_user(): 31 | print('Updating user info') 32 | wat.refresh_user_info() 33 | 34 | def autofetch(): 35 | af.run() 36 | -------------------------------------------------------------------------------- /lib/settings.py: -------------------------------------------------------------------------------- 1 | from . import database as database 2 | 3 | def update(params): 4 | try: 5 | database.update('insert or replace into settings(key, value_1, value_2) values ("' + params['setting'] + '", "' + params['value_1'] + '", "' + params['value_2'] + '")') 6 | return {'class': 'alert-success', 'message': 'Settings updated successfully.'} 7 | except: 8 | print("Error updating settings") 9 | return {'class': 'alert-error', 'message': 'Sorry but something went wrong! Check the log for details.'} 10 | 11 | def get_all(): 12 | s_list = database.row_fetch('select * from settings') 13 | s_dict = {} 14 | for setting in s_list: 15 | s_dict[setting['key']] = setting 16 | return s_dict 17 | 18 | def get(key): 19 | return database.fetch('select * from settings where key = "' + key + '"')[0] 20 | -------------------------------------------------------------------------------- /lib/torrent.py: -------------------------------------------------------------------------------- 1 | from . import database as database 2 | from . import wat as wat 3 | from . import settings as settings 4 | from . import discord as discord 5 | import json 6 | 7 | import urllib.request, urllib.error, urllib.parse 8 | 9 | def queue(data): 10 | query = 'insert into torrents(id, artist, album, release, quality, added, downloaded) values ("' + \ 11 | str(data['id']) + '", "' + \ 12 | data['artist'] + '", "' + \ 13 | data['album'] + '", "' + \ 14 | data['displayTitle'] + '", "' + \ 15 | data['media'] + " / " + data['format'] + " " + \ 16 | data['encoding'] + '", ' + \ 17 | 'datetime("now"), 0)' 18 | 19 | database.update(query) 20 | 21 | def exists(id, artist, album): 22 | query = 'select id from torrents where id = "' + str(id) + '" or artist = "' + artist + '" and album = "' + album + '"' 23 | return database.fetch(query) != [] 24 | 25 | def download_all(): 26 | torrents = database.row_fetch('select * from torrents where downloaded = 0') 27 | for t in torrents: 28 | download_torrent(t[0]) 29 | print('Downloaded ' + t[0] + '.torrent -- ' + t[1] + ' - ' + t[2] + ' / ' + t[3] + ' / ' + t[4]) 30 | discord.send('Downloaded ' + t[1] + ' - ' + t[2] + ' / ' + t[3] + ' / ' + t[4]) 31 | 32 | def download_torrent(torrent_id): 33 | download_link = wat.download_link(torrent_id) 34 | download_path = settings.get('torrent')[1] 35 | save_to = download_path + torrent_id + ".torrent" 36 | 37 | opener = urllib.request.build_opener() 38 | opener.addheaders = [('User-agent', 'Mozilla/5.0')] 39 | torrent = opener.open(download_link).read() 40 | 41 | output = open(save_to,'wb') 42 | output.write(torrent) 43 | output.close() 44 | 45 | print("Downloaded " + torrent_id + ".torrent") 46 | database.update('update torrents set downloaded = 1 where id = "' + torrent_id + '"') 47 | 48 | def get_recent(): 49 | return database.row_fetch('select * from torrents order by added desc limit 10') 50 | 51 | def get_all(): 52 | return database.row_fetch('select * from torrents order by added desc') 53 | 54 | def get_ids_for_artist(artist): 55 | return database.fetch('select id from torrents where artist = "' + artist + '"') 56 | 57 | def get_all_ids(): 58 | return database.fetch('select id from torrents') 59 | -------------------------------------------------------------------------------- /lib/wat.py: -------------------------------------------------------------------------------- 1 | from . import whatapi 2 | from . import settings as settings 3 | from . import torrent as torrents 4 | from . import database as database 5 | 6 | import json 7 | 8 | apihandle = None 9 | 10 | 11 | # Login stuff 12 | def handle(): 13 | global apihandle 14 | try: 15 | setting = settings.get('what_credentials') 16 | domain = settings.get('domain')[1] 17 | if apihandle != None: 18 | return apihandle 19 | apihandle = whatapi.WhatAPI(username=setting[1], password=setting[2], domain=domain) 20 | return apihandle 21 | except: 22 | raise Exception('Something went wrong connecting to WhatCD. Ensure that it is up and running, and that your credentials are correct.') 23 | 24 | def bust_handle_cache(): 25 | global apihandle 26 | apihandle = None 27 | 28 | 29 | # Fetching 30 | def get_artist(query): 31 | try: 32 | info = handle().request('artist', artistname=query)['response'] 33 | return handle_artist_results(info) 34 | except whatapi.whatapi.RequestException: 35 | return "no data" 36 | 37 | def get_group(group_id): 38 | try: 39 | return handle().request('torrentgroup', id=group_id)['response']['group'] 40 | except: 41 | return "no data" 42 | 43 | def browse(searchstr): 44 | try: 45 | info = handle().request('browse', searchstr=searchstr)['response'] 46 | return handle_browse_results(info) 47 | except: 48 | return "no data" 49 | 50 | def label(searchstr): 51 | try: 52 | info = handle().request('browse', recordlabel=searchstr)['response'] 53 | return handle_browse_results(info) 54 | except: 55 | return "no data" 56 | 57 | def download_link(torrent_id): 58 | domain = settings.get('domain')[1] 59 | return domain + '/torrents.php?action=download&id=' + torrent_id + '&authkey=' + handle().authkey + '&torrent_pass=' + handle().passkey 60 | 61 | def refresh_user_info(): 62 | info = handle().request('index')['response'] 63 | database.update("update user set username = '" + info['username'] + "', " 64 | "upload = '" + human_readable(info['userstats']['uploaded']) + "', " 65 | "download = '" + human_readable(info['userstats']['downloaded']) + "', " 66 | "ratio = '" + str(info['userstats']['ratio'] )+ "', " 67 | "requiredratio = '" + str(info['userstats']['requiredratio']) + "', " 68 | "class = '" + str(info['userstats']['class']) + "', " 69 | "notifications = '" + str(info['notifications']['notifications']) + "', " 70 | "newSubscriptions = '" + str(info['notifications']['newSubscriptions']) + "', " 71 | "messages = '" + str(info['notifications']['messages']) + "', " 72 | "newBlog = '" + str(info['notifications']['newBlog']) + "'" 73 | ) 74 | 75 | # Massaging 76 | def handle_browse_results(info): 77 | snatched_torrents = [] 78 | 79 | for res in info.get('results'): 80 | for torrent in res.get('torrents'): 81 | 82 | if str(torrent.get('torrentId')) in snatched_torrents: 83 | torrent['alreadySnatched'] = 1 84 | else: 85 | torrent['alreadySnatched'] = 0 86 | 87 | make_browse_title(torrent) 88 | 89 | torrent['id'] = torrent['torrentId'] 90 | torrent['size'] = human_readable(torrent['size']) 91 | torrent['artist'] = res['artist'] 92 | torrent['album'] = res['groupName'] 93 | 94 | torrent['json'] = json.dumps(torrent) 95 | 96 | return info 97 | 98 | 99 | def handle_artist_results(info): 100 | snatched_torrents = [] 101 | 102 | for group in info.get("torrentgroup", []): 103 | for torrent in group.get("torrent", []): 104 | make_artist_title(group, torrent) 105 | 106 | if str(torrent.get('id')) in snatched_torrents: 107 | torrent['alreadySnatched'] = 1 108 | else: 109 | torrent['alreadySnatched'] = 0 110 | 111 | torrent['size'] = human_readable(torrent['size']) 112 | 113 | torrent['artist'] = info['name'] 114 | torrent['album'] = group['groupName'] 115 | torrent['json'] = json.dumps(torrent) 116 | 117 | return info 118 | 119 | def make_artist_title(group, torrent): 120 | if torrent.get('remasterYear', 0) == 0: 121 | torrent['displayTitle'] = "Original Release" 122 | if group.get("groupRecordLabel") != '': 123 | torrent['displayTitle'] += " / " + group.get("groupRecordLabel") 124 | else: 125 | torrent['displayTitle'] = torrent.get('remasterTitle') + " / " 126 | torrent['displayTitle'] += torrent.get('remasterRecordLabel') 127 | 128 | def make_browse_title(torrent): 129 | if torrent.get('remasterYear', 0) == 0: 130 | torrent['displayTitle'] = "Original Release" 131 | else: 132 | torrent['displayTitle'] = torrent.get('remasterTitle') 133 | 134 | # http://stackoverflow.com/a/1094933/1855253 135 | def human_readable(size): 136 | for unit in ['','K','M','G','T','P','E','Z']: 137 | if abs(size) < 1024.0: 138 | return "%3.1f%s%s" % (size, unit, 'B') 139 | size /= 1024.0 140 | return "%.1f%s%s" % (size, 'Y', 'B') 141 | -------------------------------------------------------------------------------- /lib/whatapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .whatapi import WhatAPI 2 | 3 | __version__ = "0.1.1" 4 | -------------------------------------------------------------------------------- /lib/whatapi/whatapi.py: -------------------------------------------------------------------------------- 1 | try: 2 | from configparser import ConfigParser 3 | except ImportError: 4 | import configparser as ConfigParser # py3k support 5 | import requests 6 | import time 7 | 8 | headers = { 9 | 'Content-type': 'application/x-www-form-urlencoded', 10 | 'Accept-Charset': 'utf-8', 11 | 'User-Agent': 'whatapi [isaaczafuta]' 12 | } 13 | 14 | 15 | 16 | class LoginException(Exception): 17 | pass 18 | 19 | 20 | class RequestException(Exception): 21 | pass 22 | 23 | 24 | class WhatAPI: 25 | def __init__(self, config_file=None, username=None, password=None, cookies=None, domain=None): 26 | self.session = requests.Session() 27 | self.session.headers = headers 28 | self.authkey = None 29 | self.passkey = None 30 | self.domain = domain 31 | if config_file: 32 | config = ConfigParser() 33 | config.read(config_file) 34 | self.username = config.get('login', 'username') 35 | self.password = config.get('login', 'password') 36 | else: 37 | self.username = username 38 | self.password = password 39 | if cookies: 40 | self.session.cookies = cookies 41 | try: 42 | self._auth() 43 | except RequestException: 44 | self._login() 45 | else: 46 | self._login() 47 | 48 | def _auth(self): 49 | '''Gets auth key from server''' 50 | accountinfo = self.request("index") 51 | self.authkey = accountinfo["response"]["authkey"] 52 | self.passkey = accountinfo["response"]["passkey"] 53 | 54 | def _login(self): 55 | '''Logs in user''' 56 | loginpage = self.domain + '/login.php' 57 | data = {'username': self.username, 58 | 'password': self.password, 59 | 'keeplogged': 1, 60 | 'login': 'Login' 61 | } 62 | r = self.session.post(loginpage, data=data, allow_redirects=False) 63 | if r.status_code != 302: 64 | raise LoginException 65 | self._auth() 66 | 67 | def get_torrent(self, torrent_id): 68 | '''Downloads the torrent at torrent_id using the authkey and passkey''' 69 | torrentpage = self.domain + '/torrents.php' 70 | params = {'action': 'download', 'id': torrent_id} 71 | if self.authkey: 72 | params['authkey'] = self.authkey 73 | params['torrent_pass'] = self.passkey 74 | r = self.session.get(torrentpage, params=params, allow_redirects=False) 75 | time.sleep(2) 76 | if r.status_code == 200 and 'application/x-bittorrent' in r.headers['content-type']: 77 | return r.content 78 | return None 79 | 80 | def logout(self): 81 | '''Logs out user''' 82 | logoutpage = self.domain + '/logout.php' 83 | params = {'auth': self.authkey} 84 | self.session.get(logoutpage, params=params, allow_redirects=False) 85 | 86 | def request(self, action, **kwargs): 87 | '''Makes an AJAX request at a given action page''' 88 | ajaxpage = self.domain + '/ajax.php' 89 | params = {'action': action} 90 | if self.authkey: 91 | params['auth'] = self.authkey 92 | params.update(kwargs) 93 | 94 | r = self.session.get(ajaxpage, params=params, allow_redirects=False) 95 | time.sleep(2) 96 | try: 97 | json_response = r.json() 98 | if json_response["status"] != "success": 99 | raise RequestException 100 | return json_response 101 | except ValueError: 102 | raise RequestException 103 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/rRBAXAU.png) 2 | 3 | GazelleUI is a web based torrent manager a-la CouchPotato, Headphones, or SickRage, but just for [Gazelle](https://github.com/WhatCD/Gazelle) based music trackers (such as APL or PTH). It serves as a web wrapper for Gazelle's API, and downloads torrents into a folder that can be 'watched' by any torrent client for instant downloading. 4 | 5 | It's minimal, it's fast, and it works on your phone. What more do you want. 6 | 7 | [Check out these awesome screenshots](https://imgur.com/a/fZysf) 8 | 9 | ## Installation 10 | 11 | If you've got it, [Docker](https://www.docker.com/) is the best way to run GazelleUI. 12 | 13 | docker create \ 14 | --name=gazelleui \ 15 | --restart always \ 16 | -v :/torrents \ 17 | -v :/app/config \ 18 | -e PGID=1000 -e PUID=1000 \ 19 | -e TZ=America/Los_Angeles \ 20 | -p 2020:2020 \ 21 | xanderstrike/gazelleui 22 | 23 | * Set the watchfolder to a directory watched by your torrent client 24 | * PGID and PUID can be found by running `id` in a terminal 25 | * Timezone is your timezone 26 | * Configure the port by setting `2020:2020` to `:2020` 27 | 28 | Run with: 29 | 30 | docker start gazelleui 31 | 32 | ### Without Docker or for Development 33 | 34 | GazelleUI is designed with seedboxes in mind. These instructions are for Ubuntu, but it'll run on most any linux or osx box. 35 | 36 | Download or clone this repository. 37 | 38 | Set up the prerequisites and run: 39 | 40 | sudo apt-get update 41 | sudo apt-get install wget python3-pip 42 | sudo pip install -r requirements.txt 43 | python GazelleUI.py 44 | 45 | 46 | Then visit `:2020` to set it up! 47 | 48 | ### License 49 | 50 | MIT license 51 | 52 | Logo credit Focus Lab via NounProject: https://thenounproject.com/search/?q=gazelle&i=549876 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | funcsigs 3 | futures 4 | flask 5 | flask_apscheduler 6 | -------------------------------------------------------------------------------- /templates/_snatch_table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for t in torrents %} 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | {% endfor %} 27 | 28 |
ArtistReleaseQualityAddedSnatched
{{ t[1] }}{{ t[2] }} / {{ t[3] }}{{ t[4] }}{{ t[5] }} 19 | {% if t[6] == 0 %} 20 | 21 | {% else %} 22 | 23 | {% endif %} 24 |
29 | -------------------------------------------------------------------------------- /templates/artist.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 | 6 |
7 |
8 |
9 |

{{ results.name | safe }}

10 |

{{ results.body | safe}}

11 |
12 |
13 | More 14 | Less 15 |
16 |
17 |
18 | 19 | 35 | 36 | 37 | 38 | {% for res in results.torrentgroup %} 39 |
40 |

{{ res.groupYear }} - {{ res.groupName | safe }}

41 | More Info 42 | Less Info 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% for torrent in res.torrent %} 61 | 62 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 85 | 86 | {% endfor %} 87 | 88 |
FormatFilesSizeAddedSnatchedSeedersLeechers
63 | {{ torrent.displayTitle }}
64 | {{ torrent.media }} / {{ torrent.format }} {{ torrent.encoding }} 65 | {% if torrent.hasLog %} 66 | / {{ torrent.logScore }}% 67 | {% endif %} 68 | {% if torrent.hasCue %} 69 | / Cue 70 | {% endif %} 71 |
{{ torrent.fileCount }}{{ torrent.size }}{{ torrent.time }}{{ torrent.snatched }}{{ torrent.seeders }}{{ torrent.leechers }} 79 | {% if torrent.alreadySnatched == 1 %} 80 | 81 | {% else %} 82 | Download 83 | {% endif %} 84 |
89 |
90 |
91 | More 92 |
93 |
94 | {% endfor %} 95 | {% endblock %} 96 | -------------------------------------------------------------------------------- /templates/browse.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

Results for "{{ query }}"

4 | 5 | 6 | {% for res in results.results %} 7 |
8 |

{{ res.artist }} - {{ res.groupName }}

9 | 10 |
11 | 12 | {% for torrent in res.torrents %} 13 | 14 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | {% endfor %} 39 |
15 | {{ torrent.displayTitle }}
16 | {{ torrent.media }} / {{ torrent.format }} {{ torrent.encoding }} 17 | {% if torrent.hasLog %} 18 | / {{ torrent.logScore }}% 19 | {% endif %} 20 | {% if torrent.hasCue %} 21 | / Cue 22 | {% endif %} 23 |
{{ torrent.fileCount }}{{ torrent.size }}{{ torrent.time }}{{ torrent.snatched }}{{ torrent.seeders }}{{ torrent.leechers }} 31 | {% if torrent.alreadySnatched == 1 %} 32 | 33 | {% else %} 34 | Download 35 | {% endif %} 36 |
40 |
41 | 42 |
43 | More 44 |
45 |
46 |
47 | {% endfor %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/group_info.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {{ group_info.wikiBody | safe}} 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Category{{ group_info.category }}
Time{{ group_info.time }}
Label{{ group_info.recordLabel }}
Tags{{ ", ".join(group_info.tags) }}
29 |
30 |
31 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 |

Recent Snatches

5 | 6 | {% include "_snatch_table.html" %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GazelleUI 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 | {% include "shared/_filters.html" %} 39 | 40 |
41 | {% block body %}{% endblock %} 42 |
43 | 44 |
45 |

46 | {% if userinfo is defined %} 47 | {{ userinfo[0] }} / Up: {{ userinfo[1] }} / Down: {{ userinfo[2] }} / Ratio: {{ userinfo[3] }} / Required: {{ userinfo[4] }} / 48 | {% endif %} 49 | Snatches / Settings / Subscriptions 50 |

51 |
52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 | {% if message != None %} 5 |

{{ message }}

6 | {% endif %} 7 | 8 |
9 |
10 |

GazelleUI Credentials

11 |

The username and password you'd like to use to log in here.

12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |

Tracker Domain

24 |

The domain of the site you use to log in, including protocol (https://apollo.rip, https://passtheheadphones.me).

25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |

Tracker Credentials

35 |

The username and password that you use to log in to the site.

36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 |

Torrents

49 |

Torrent settings

50 |
51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 |

Network

59 |

The what port to listen on and who to respond to. Requires a restart. Defaults are fine for most users.

60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 |

Discord Integration

71 |

Post to this webhook URL when a download starts.

72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
81 |
82 | 83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /templates/shared/_filters.html: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |
Loading...
32 |
33 | -------------------------------------------------------------------------------- /templates/snatches.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

All Snatches

4 | 5 | {% include "_snatch_table.html" %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/subscriptions.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

Subscriptions

4 | 5 |
6 | 10 | 11 | 12 | 13 | 18 | 19 | 34 | 35 | 36 |
37 | 38 |

Subscriptions are checked every 12 hours.

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% for sub in subs %} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% endfor %} 61 | 62 |
TypeTermQualityRelease Type
{{ sub['search_type'] }}{{ sub['term'] }}{{ sub['quality'] }}{{ sub['release_type'] }}Delete
63 | 64 | 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /torrents/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanderStrike/GazelleUI/34af5c24f5c2a60687973aaace5bbf98b1ccb392/torrents/.keep -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from WhatUI import app 2 | 3 | if __name__ == "__main__": 4 | app.run() 5 | --------------------------------------------------------------------------------