├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── _webpack_runner.py ├── better-image-upload └── src │ └── ab_better_image_upload.user.js ├── delicious-library ├── README.md └── src │ └── ab_delicious_library.js ├── delicious-userscripts ├── README.md ├── _header.js ├── _update_template.py ├── dist │ └── ab_delicious_scripts.user.js ├── old │ ├── _ab_delicious_scripts_loader.user.js │ ├── _ab_delicious_template.js │ ├── _delicious_common.js │ ├── ab_dynamic_stylesheets.user.js │ ├── ab_paragraph_spacing.user.js │ ├── assemble_delicious_script.py │ ├── before_library.txt │ ├── concat_userscripts.py │ ├── create_dev_userscript.py │ ├── delicious_1.967.user.js │ └── localStorage_keys.txt └── src │ ├── ab_enhanced_torrent_view.user.js │ ├── ab_fl_status.user.js │ ├── ab_forum_search_enhancement.user.js │ ├── ab_hide_treats.user.js │ ├── ab_hyper_quote.user.js │ ├── ab_keyboard_shortcuts.user.js │ ├── ab_title_inverter.user.js │ ├── ab_title_notifications.user.js │ └── ab_yen_stats.user.js ├── highlights-2 ├── css │ └── tfm_highlights.user.css ├── dist │ ├── ab_highlights_2.user.js │ ├── bundle.js │ └── bundle.js.map ├── src │ ├── highlighter.ts │ ├── lexer.ts │ ├── parser.ts │ └── types.ts ├── test │ ├── fate_series.json │ └── index.html └── tsconfig.json ├── package-lock.json ├── package.json ├── quick-links └── src │ └── ab_quick_links.user.js ├── torrent-highlighter ├── .babelrc ├── README.md ├── css │ ├── tfm_torrent_styles.user.css │ └── torrent_styles.user.css ├── dist │ ├── tfm_torrent_highlighter.min.user.js │ ├── tfm_torrent_highlighter.min.user.js.map │ ├── tfm_torrent_highlighter.user.js │ └── tfm_torrent_highlighter.user.js.map ├── src │ └── tfm_torrent_highlighter.user.js ├── test_data_fate_series.txt ├── webpack.config.js └── webpack_min.config.js ├── torrent-sorter ├── .babelrc ├── README.md ├── dist │ ├── ab_torrent_sorter.min.user.js │ ├── ab_torrent_sorter.min.user.js.map │ ├── ab_torrent_sorter.user.js │ └── ab_torrent_sorter.user.js.map ├── src │ └── ab_torrent_sorter.user.js ├── webpack.config.js └── webpack_min.config.js ├── vector-logo └── src │ └── ab_vector_logo.user.js └── webpack_base.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | */dist/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "greasemonkey": true, 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6 9 | }, 10 | "globals": { 11 | "delicious": false 12 | }, 13 | "extends": "eslint:recommended", 14 | "rules": { 15 | "indent": [ 16 | "warn", 17 | 4 18 | ], 19 | "linebreak-style": [ 20 | "off", 21 | ], 22 | "semi": [ 23 | "error", 24 | "always" 25 | ], 26 | "no-console": [ 27 | "off" 28 | ] 29 | } 30 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ropeproject/ 2 | node_modules/ 3 | __pycache__/ 4 | delicious-userscripts/dist/ab_delicious_scripts.dev.user.js 5 | delicious-userscripts/before_library.txt 6 | *.dev.user.js -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}" 12 | }, 13 | { 14 | "name": "Python: Attach", 15 | "type": "python", 16 | "request": "attach", 17 | "localRoot": "${workspaceFolder}", 18 | "remoteRoot": "${workspaceFolder}", 19 | "port": 3000, 20 | "secret": "my_secret", 21 | "host": "localhost" 22 | }, 23 | { 24 | "name": "Python: Terminal (integrated)", 25 | "type": "python", 26 | "request": "launch", 27 | "program": "${file}", 28 | "console": "integratedTerminal" 29 | }, 30 | { 31 | "name": "Python: Terminal (external)", 32 | "type": "python", 33 | "request": "launch", 34 | "program": "${file}", 35 | "console": "externalTerminal" 36 | }, 37 | { 38 | "name": "Python: Django", 39 | "type": "python", 40 | "request": "launch", 41 | "program": "${workspaceFolder}/manage.py", 42 | "args": [ 43 | "runserver", 44 | "--noreload", 45 | "--nothreading" 46 | ], 47 | "debugOptions": [ 48 | "RedirectOutput", 49 | "Django" 50 | ] 51 | }, 52 | { 53 | "name": "Python: Flask (0.11.x or later)", 54 | "type": "python", 55 | "request": "launch", 56 | "module": "flask", 57 | "env": { 58 | "FLASK_APP": "app.py" 59 | }, 60 | "args": [ 61 | "run", 62 | "--no-debugger", 63 | "--no-reload" 64 | ] 65 | }, 66 | { 67 | "name": "Python: Module", 68 | "type": "python", 69 | "request": "launch", 70 | "module": "module.name" 71 | }, 72 | { 73 | "name": "Python: Pyramid", 74 | "type": "python", 75 | "request": "launch", 76 | "args": [ 77 | "${workspaceFolder}/development.ini" 78 | ], 79 | "debugOptions": [ 80 | "RedirectOutput", 81 | "Pyramid" 82 | ] 83 | }, 84 | { 85 | "name": "Python: Watson", 86 | "type": "python", 87 | "request": "launch", 88 | "program": "${workspaceFolder}/console.py", 89 | "args": [ 90 | "dev", 91 | "runserver", 92 | "--noreload=True" 93 | ] 94 | }, 95 | { 96 | "name": "Python: All debug Options", 97 | "type": "python", 98 | "request": "launch", 99 | "python": "${command:python.interpreterPath}", 100 | "program": "${file}", 101 | "module": "module.name", 102 | "env": { 103 | "VAR1": "1", 104 | "VAR2": "2" 105 | }, 106 | "envFile": "${workspaceFolder}/.env", 107 | "args": [ 108 | "arg1", 109 | "arg2" 110 | ], 111 | "debugOptions": [ 112 | "RedirectOutput" 113 | ] 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabled": false, 3 | "eslint.enable": true, 4 | "files.trimTrailingWhitespace": true, 5 | "editor.tabSize": 2 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "process", 8 | "label": "Webpack current file", 9 | "command": "${config:python.pythonPath}", 10 | "args": [ 11 | "${workspaceFolder}/_webpack_runner.py", 12 | "${fileDirname}" 13 | ], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Assorted userscripts for AnimeBytes 2 | 3 | Collected together by TheFallingMan. -------------------------------------------------------------------------------- /_webpack_runner.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pathlib 3 | import subprocess 4 | import os 5 | import os.path 6 | 7 | if __name__ == '__main__': 8 | folderPath = pathlib.PurePath(sys.argv[1]) 9 | if folderPath.parts[-1] in ('dist', 'src'): 10 | folderPath = folderPath.parent 11 | elif folderPath.parts[-1] == 'ab-userscripts': 12 | print('Incorrect folder specified: ' + str(folderPath), file=sys.stderr) 13 | sys.exit(1) 14 | 15 | if folderPath.parts[-1] == 'delicious-userscripts': 16 | sys.path.append(str(folderPath)) 17 | sys.path.insert(0, './delicious-userscripts') 18 | import concat_userscripts 19 | print('Assembling delicious bundle.') 20 | concat_userscripts._main() 21 | else: 22 | webpack = folderPath / '../node_modules/.bin/webpack.cmd' 23 | for f in os.listdir(str(folderPath)): 24 | f = os.path.basename(f) 25 | if f.startswith('webpack') and f.endswith('.config.js'): 26 | print('Executing webpack config: ' + str(f)) 27 | subprocess.run( 28 | [str(webpack), '--config', str(folderPath / f)], 29 | check=True 30 | ) -------------------------------------------------------------------------------- /delicious-library/README.md: -------------------------------------------------------------------------------- 1 | # Delicious Userscript Library 2 | 3 | A library for userscripts on AnimeBytes which implements a settings page, 4 | among other things. 5 | 6 | You can use this library from a userscript by adding 7 | ``` 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | ``` 12 | to your header. 13 | 14 | ### Example Usage 15 | 16 | - [Hide Treats](../delicious-userscripts/src/ab_hide_treats.user.js) — Most basic checkbox. 17 | - [FL Pool Status](../delicious-userscripts/src/ab_fl_status.user.js) — Script section containing drop down and fieldset. 18 | - [Forum Search Enhancements](../delicious-userscripts/src/ab_forum_search_enhancement.user.js) — Script section containing checkbox, text and colour settings. 19 | - [Quick Links](../quick-links/src/ab_quick_links.user.js) — Script section with multi-row setting. 20 | 21 | ## Settings 22 | 23 | Contains everything related to generating, displaying, and storing and 24 | retrieving settings for userscripts. 25 | 26 | ### Overview 27 | 28 | By default, the library creates and manages one tab (Userscript Settings) within your profile settings. At the top is a basic section for checkboxes, for scripts which only need an on/off setting. Underneath these are individual sections for larger userscripts needing more complex settings. 29 | 30 | All these settings are saved when clicking the "Save Profile" button. 31 | 32 | ### Get and Set Settings 33 | 34 | There are 3 functions relating to storing and retrieving settings within code. 35 | 36 | - `delicious.settings.init(key, defaultValue)` — If `key` is not set to any value, sets it to `defaultValue`. Otherwise, does nothing. This is intended to set default values for settings, avoiding the need to specify a default whenever the setting is retrieved via `settings.get` or `GM_getValue`. 37 | - `delicious.settings.get(key, defaultValue)` — If `key` is set, returns its value. Otherwise, returns `defaultValue`. 38 | - `delicious.settings.set(key, value)` — Sets `key` to `value`. 39 | 40 | > **Note.** Internally, GM_setValue/GM_getValue are used but values are JSON encoded so types are preserved. For example, 41 | > ```js 42 | > delicious.settings.set('Test', true); 43 | > delicious.settings.get('Test'); // Returns boolean true, not the string true. 44 | > delicious.settings.set('Test2', {key: [1, 2, 3]}); 45 | > delicious.settings.get('Test2'); // Returns {key: [1, 2, 3]}. 46 | > ``` 47 | 48 | ### Basic Usage 49 | 50 | The simplest setting is a single on/off checkbox. For example, 51 | ```js 52 | var enabled = delicious.settings.basicScriptCheckbox( 53 | 'SettingKey', 54 | 'Script Title', 55 | 'A short description of your script' 56 | ); 57 | if (!enabled) 58 | return; 59 | 60 | // Rest of userscript... 61 | ``` 62 | This will create and insert a checkbox into the basic section. `basicScriptCheckbox()` returns the value of `SettingKey`, so if the checkbox is disabled, it wont run the rest of the script. 63 | 64 | > **Note.** This basic section is sorted alphabetically. This means there is no guarantee that two settings will be adjacent to each other. If you have two or more related settings, it is recommended to use a separate section (see below). 65 | 66 | ### Ensure Settings Inserted 67 | 68 | **Important.** Wrap all settings page related activity inside `settings.ensureSettingsInserted`. 69 | This checks the URL is a profile settings page, and ensure the settings page is inserted and valid. 70 | For example, 71 | ```js 72 | delicious.settings.init('Key', true); 73 | if (delicious.settings.ensureSettingsInserted()) { 74 | var section = delicious.settings.createSection('Script Section'); 75 | // Do whatever... 76 | delicious.settings.insertSection(section); 77 | } 78 | // Rest of script. 79 | ``` 80 | This is not required for merely accessing/storing values (`get`, `set`, etc.). Also, 81 | this is not needed for `basicScriptCheckbox`, as it does this automatically. 82 | 83 | 84 | ### Custom Script Sections 85 | 86 | If your userscript has more than one setting, I recommend you create a section to keep everything tidy and grouped. To do this, you have a few options: 87 | 88 | - `addScriptSection()` will create, insert and return a section element. Additionally, it can insert an "Enable/Disable" checkbox as the first setting in this section (use `checkbox: true` in `options`). 89 | - `createSection()` will create and return a section element. You will need to insert it into the page with `insertSection()`. **Note:** It is recommended to use collapsible sections instead, 90 | explained in the next section. 91 | 92 | In both cases, the function returns a HTMLElement. To add settings, use any of the `settings.create*` functions, then append that element. For example, to add a text box, 93 | ```js 94 | var section = delicious.settings.createSection('Foo Script'); // Create a setting section. 95 | 96 | section.appendChild(delicious.settings.createTextSetting('FooText', 'Text for foo', 'Enter some text to be fooed'); 97 | // Add more settings if needed... 98 | 99 | delicious.settings.insertSection(section); // Insert onto the settings page. 100 | ``` 101 | 102 | #### Collapsible Sections 103 | 104 | You can create a script section which can be collapsed/expanded to save space, 105 | resulting in a neater Userscript Settings page. To do this, use this function: 106 | 107 | - `createCollapsibleSection(title, defaultState)` will create and return a 108 | collapsible section element. If `defaultState` is true, it will be shown by default. 109 | Otherwise, it will be collapsed by default. You will need to insert this section 110 | into the page with `insertSection()`. 111 | 112 | **Important.** Appending directly into the returned div element will 113 | not work. You must append to the section's body div. 114 | 115 | Correct example: 116 | ```js 117 | var section = delicious.settings.createCollapsibleSection('Script Name'); 118 | var sectionBody = section.querySelector('.settings_section_body'); 119 | sectionBody.appendChild(delicious.settings.createCheckbox( ... )); 120 | delicious.setttings.insertSection(section); 121 | ``` 122 | 123 | Incorrect example: 124 | ```js 125 | var section = delicious.settings.createCollapsibleSection('Script Name'); 126 | // This will not be able to collapse/expand the section correctly! 127 | section.appendChild(delicious.settings.createCheckbox( ... )); 128 | delicious.setttings.insertSection(section); 129 | ``` 130 | 131 | Apart from this, it looks and functions identically to a normal section. 132 | 133 | ### Available Settings 134 | 135 | In general, the `settings.add*` functions create and insert a setting element for you. `settings.create*` return settings elements which you need to insert somewhere. `settings.insert*` inserts a given setting element. 136 | 137 | ### `settings.add*` 138 | 139 | The following `add` functions are available (see source code for more details, and the delicious bundle for examples): 140 | 141 | - `addBasicCheckbox(key, label, description, options)` — Creates and inserts a checkbox to the basic section. 142 | 143 | ### `settings.insert*` 144 | 145 | The following `insert` functions are available: 146 | 147 | - `insertBasicSetting(setting)` — Inserts `setting` into the basic section. 148 | 149 | - `insertSection(section)` — Inserts `section`, a HTML element containing a script section, into the settings page. 150 | 151 | - `insertSettingsPage(label, settingsPage)` — Inserts `settingsPage` onto the user settings page, with `label` as the tab bar label. This shouldn't be needed for most scripts. 152 | 153 | ### `settings.create*` 154 | 155 | > **Note.** Specifying a default when creating a setting _does not_ store that 156 | > value into the setting until "Save Profile" is clicked. 157 | > This results in the following behaviour: 158 | > ```js 159 | > settings.createTextSetting('TextSetting', 'Text', 'Some text', 160 | > {default: "Default Value"}); 161 | > settings.get('TextSetting'); // Returns undefined. 162 | > ``` 163 | > You can either specify a default when calling `settings.get` or use 164 | > `settings.init` (see above) to store the default. If you use `settings.init`, 165 | > specifying the default when creating the setting would be unnecessary. 166 | > ```js 167 | > settings.init('TextSetting', 'Default Value'); 168 | > // Text setting will still have "Default Value" as default. 169 | > settings.createTextSetting('TextSetting', 'Text', 'Some text'); 170 | > settings.get('TextSetting'); // Returns "Default Value". 171 | > ``` 172 | 173 | The following `create` functions are provided. `key` is the setting key to store in, `label` is the label in the left column, `description` is placed on the right side near the input element. A default can be specified with `{default: ...}` as `options`. The given default value must match the setting's format. Some `options` properties are mentioned, see source code for more. 174 | 175 | In all cases, you will need to insert the returned setting element onto the page yourself. 176 | 177 | - `createCheckbox(key, label, description, options)` - Creates a checkbox setting. 178 | 179 | - `createSection(title)` — Creates and returns a setting section with the given title as a heading. 180 | Use `appendChild` on the returned section to add setting elements. 181 | 182 | - `createTextSetting(key, label, description, options)` — Creates a text box setting. 183 | - `createDropDown(key, label, description, valuesArray, options)` — Creates a drop-down setting. `valuesArray` is an array of 2-tuples which are `[label, value]` (both should be strings). `label` will be shown in the dropdown. When that option is selected, its corresponding `value` will be stored. If given, the default property should be a `value`. 184 | - `createNumberInput(key, label, description, options)` — Creats a numeric setting. Value will be stored as a number (empty inputs will be stored as null). Within options, `allowDecimal` (default true) and `allowNegative` (default false) work as expected, `required` (default false) is whether input must be non-empty. 185 | - `createFieldSetSetting(key, label, fields, description, options)` — Creates a field set setting (essentially a row of many checkboxes). Value is stored in `key` as an object with boolean properties. `default` should be a similar object. 186 | `fields` is an array of 2-tuples `[text, subkey]` (both strings). `text` is a short label for each checkbox, `subkey` is the property this checkbox is stored under. For example, 187 | ```js 188 | // Creates a setting with 2 checkboxes. 189 | delicious.settings.createFieldSetSetting('FLPoolLocations', 190 | 'Freeleech status locations', 191 | [['Navbar', 'navbar'], ['User menu', 'usermenu']]); 192 | // Example stored value 193 | delicious.settings.get('FLPoolLocations') == { 194 | 'navbar': true, 195 | 'usermenu': false 196 | }; 197 | ``` 198 | 199 | - `createRowSetting(key, label, columns, description, options)` — Creates a multi-row setting. That is, a setting with certain columns and a variable number of rows. `columns` is an array of 3-tuples of `[column label, subkey, type]` (all strings, type should be `text` or `number`). Value (and `options.default`) is stored as an array of objects. Each object represents one row, with the specified subkeys as properties and the cell value as property values. In `options`, 200 | 201 | - `allowSort` (default true) allows reordering rows with provided buttons, 202 | - `allowDelete` (default true) allows deleting rows, 203 | - `allowNew` (default true) allows creating new rows, and 204 | - `newButtonText` (default `+`) is the text on the add row button. 205 | 206 | You can require a certain number of rows by specifying an appropriate default and disabling new and delete. 207 | 208 | - `createColourSetting(key, label, description, options)` — Creates a colour input, with optional checkbox to enable/disable the whole setting and a reset to default button. Value is stored as a hex string `#rrggbb`, or null if checkbox is unchecked. In `options`, 209 | 210 | - `checkbox` (default true) displays the checkbox, and 211 | - `resetButton` (default true) displays the reset button. 212 | 213 | Also, 214 | 215 | - `showErrorMessage(message, errorId)` — Shows an error message in a friendly red box near the top of the page. `errorId` should be a string identifying the type of error, used to remove previous errors of the same type before displaying the new error. 216 | 217 | ### Custom Save Handlers 218 | 219 | On clicking the "Save Profile" button, a `deliciousSave` event is sent to all 220 | elements with a `data-setting-key` attribute. The attached event handler is 221 | responsible for storing the setting value. 222 | 223 | If, for whatever reason, the setting cannot be saved, the event handler can 224 | call `event.preventDefault()` which will prevent the form from being submitted. 225 | `settings.showErrorMessage` is provided to display an error message. 226 | 227 | ### Custom Settings Pages 228 | 229 | It is possible to create and use a completely new settings page. Refer to: 230 | 231 | - `settings._createDeliciousPage` — Creates the delicious settings page, with required structure and sections. 232 | - `settings._insertDeliciousSettings` — Creates and inserts the delicious settings page, attaching event handlers. 233 | - `settings.insertSettingsPage` — Inserts a setting page into the user profile settings. 234 | 235 | ## Utilities 236 | 237 | Various utility functions related to AnimeBytes. 238 | 239 | - `toggleSubnav(ev)` — Event handler to be attached to the little drop-down triangles. 240 | Toggles displaying the adjacent menu. 241 | 242 | - `applyDefaults(options, defaults)` — Accepts two objects with properties. Returns an 243 | objetc combining the two, using values from `options` if present, `default` otherwise. 244 | Essentially used to define defaults on parameters specified as objects. 245 | 246 | - `htmlEscape(text)` — Returns `text` escaped for HTML (e.g. `&` is replaced with `&`). 247 | 248 | - `parseBytes(bytesString)` — Accepts `bytesString`, a string containing a 249 | number and a bytes unit. Returns the number of bytes. **Note:** Unit must use 250 | IEC prefixes (KiB, GiB, etc.). 251 | 252 | - `formatBytes(numBytes, decimals)` — Formats a number of bytes as a string 253 | with an appropriate unit, to a certain number of decimal places. 254 | 255 | - `toDataAttr(str)` — Given an `element.dataset` property name in camelCase, 256 | returns the corresponding `data-` attribute name with hyphens. 257 | -------------------------------------------------------------------------------- /delicious-userscripts/README.md: -------------------------------------------------------------------------------- 1 | # Delicious Userscripts Bundle 2 | 3 | The `src/` folder contains the individual userscripts, all fully functional. 4 | The `dist/` folder contains the bundled script, generated using Python. 5 | 6 | ### Development 7 | 8 | The source code for individual userscripts are in the src folder. 9 | Please make sure that each script works by itself by including a suitable 10 | userscript header in its file. 11 | 12 | To create the bundle in dist/ab_delicious_scripts.user.js, the individual 13 | scripts are merged together with the Python script _update_template.py. 14 | The header of the bundle itself is in the _header.js file. 15 | 16 | Once the changes are made to individual scripts in src/, execute the 17 | _update_template.py script to concatenate them into the combined script. 18 | This will also add their `@require` and `@grant` tags to the _header.js 19 | file. 20 | 21 | A summary of the steps is below. 22 | 23 | 1. Make changes to individual scripts as needed and bump version number. 24 | 2. Bump version number in _header.js. 25 | 3. Generate dist/ab_delicious_scripts.user.js by running _update_template.py. 26 | For example, 27 | 28 | python3 _update_template.py 29 | 30 | 4. Commit and push / pull request as usual. 31 | -------------------------------------------------------------------------------- /delicious-userscripts/_header.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes delicious user scripts (updated) 3 | // @author aldy, potatoe, alpha, Megure 4 | // @version 2.1.12 5 | // @description Userscripts to enhance AnimeBytes in various ways. (Updated by TheFallingMan) 6 | // @match https://*.animebytes.tv/* 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // @require https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.8.23/dayjs.min.js 11 | // @require https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.8.23/plugin/customParseFormat.js 12 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 13 | // ==/UserScript== 14 | 15 | /** 16 | * This is the bundle metadata header and should be *manually* updated when 17 | * a userscript @require's more scripts or updates to a new version. 18 | * 19 | * The automated python script will alter the "grant" and "require" tags above 20 | * depending on the individual scripts. 21 | */ 22 | -------------------------------------------------------------------------------- /delicious-userscripts/_update_template.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from collections import defaultdict 3 | import itertools 4 | import os 5 | 6 | KEYS = {'@require', '@grant'} 7 | 8 | def userscript_header(lines): 9 | # header is None before header, True during and False after. 10 | header = None 11 | for l in lines: 12 | if header is None and '==UserScript==' in l: 13 | header = True 14 | yield l 15 | elif header is True and '==/UserScript==' in l: 16 | header = False 17 | yield l 18 | elif header is True: 19 | yield l 20 | else: 21 | return 22 | 23 | def try_parse_option(line): 24 | l = line.split() 25 | if len(l) < 3: return (None, None) 26 | slashes, key, value, *rest = l 27 | if slashes != '//' or key not in KEYS: return (None, None) 28 | if rest: raise ValueError("Invalid userscript @" + key + " line: " + l) 29 | return key, value 30 | 31 | 32 | def parse_header(header): 33 | data = defaultdict(list) 34 | for l in header: 35 | k, v = try_parse_option(l) 36 | if k: 37 | data[k].append(v) 38 | return data 39 | 40 | def replace_header(lines, data): 41 | ignore = None 42 | for l in lines: 43 | k, v = try_parse_option(l) 44 | if k not in data: 45 | yield l 46 | continue 47 | if k == ignore: 48 | continue 49 | index = l.index(v) 50 | left = ('// ' + k).ljust(index) 51 | for val in data[k]: 52 | yield left + val + '\n' 53 | ignore = k 54 | 55 | 56 | if __name__ == '__main__': 57 | data = defaultdict(list) 58 | HEADER = '_header.js' 59 | SRC = './src' 60 | scripts = [] 61 | for name in os.listdir(SRC): 62 | f = os.path.join(SRC, name) 63 | if not f.lower().endswith('.user.js'): continue 64 | if not os.path.isfile(f): continue 65 | 66 | scripts.append(SRC + '/' + name) 67 | 68 | with open(f) as fo: 69 | for k, vs in parse_header(userscript_header(fo)).items(): 70 | data[k].extend(x for x in vs if x not in data[k]) 71 | 72 | with open(HEADER) as fo: 73 | new_lines = list(replace_header(fo, data)) 74 | with open(HEADER, 'w') as fo: 75 | fo.writelines(new_lines) 76 | 77 | # just concatenate all the files here ;-; 78 | dist_file = 'dist/ab_delicious_scripts.user.js' 79 | with open(dist_file, 'wb') as f: 80 | for part in [HEADER] + scripts: 81 | with open(part, 'rb') as f2: 82 | f.writelines(f2.readlines()) 83 | f.write(b'\n') 84 | f.write(b'// Compiled ' + datetime.now(timezone.utc).isoformat().encode('utf-8') + b'.') 85 | 86 | # make_ref = lambda s: '/// ' 87 | 88 | # template_lines = [] 89 | # write = lambda x: template_lines.append(x + '\n') 90 | # write_lines = lambda l: write('\n'.join(l)) 91 | 92 | # write_lines([ 93 | # '// This file is automatically generated and any changes will be overwritten.', 94 | # "// Please do not manually edit this file.", 95 | # '// Template last modified ' + datetime.now(timezone.utc).isoformat() + '.' 96 | # ]) 97 | # write('') 98 | # write_lines(make_ref(s) for s in [HEADER] + scripts) 99 | 100 | # with open('_template.ts', 'w') as fo: 101 | # fo.writelines(template_lines) 102 | -------------------------------------------------------------------------------- /delicious-userscripts/old/_ab_delicious_scripts_loader.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes delicious user scripts (updated) 3 | // @author aldy, potatoe, alpha, Megure 4 | // @version 2.0.1.9 5 | // @description Variety of userscripts to fully utilise the site and stylesheet. (Updated by TheFallingMan) 6 | // @grant GM_getValue 7 | // @grant GM_setValue 8 | // @grant GM_deleteValue 9 | // @include *animebytes.tv/* 10 | // @match https://*.animebytes.tv/* 11 | // @icon http://animebytes.tv/favicon.ico 12 | // @require https://raw.githubusercontent.com/momentary0/AB-Userscripts/master/delicious-library/src/ab_delicious_library.js 13 | // ==/UserScript== 14 | 15 | (function AnimeBytesDeliciousUserScripts() { 16 | 17 | // Placeholder function. Imports are done by an external Python script 18 | // inserting script files where needed. 19 | // This should never be executed. 20 | function importScriptFile(filename) { 21 | console.error(filename + ' was not imported into the delicious userscript'); 22 | } 23 | 24 | // Some GM_ functions and Javascript polyfills 25 | 26 | 27 | // Better Quote no longer necessary. 28 | 29 | // HYPER QUOTE by Megure 30 | // Select text and press CTRL+V to quote 31 | importScriptFile('ab_hyper_quote.user.js'); 32 | 33 | 34 | // Forums title inverter by Potatoe 35 | // Inverts the forums titles. 36 | importScriptFile('ab_title_inverter.user.js'); 37 | 38 | 39 | // Hide treats by Alpha 40 | // Hide treats on profile. 41 | importScriptFile('ab_hide_treats.user.js'); 42 | 43 | 44 | // Keyboard shortcuts by Alpha, mod by Megure 45 | // Enables keyboard shortcuts for forum (new post and edit) and PM 46 | importScriptFile('ab_keyboard_shortcuts.user.js'); 47 | 48 | 49 | // Title Notifications by Megure 50 | // Will prepend the number of notifications to the title 51 | importScriptFile('ab_title_notifications.user.js'); 52 | 53 | 54 | // Freeleech Pool Status by Megure, inspired by Lemma, Alpha, NSC 55 | // Shows current freeleech pool status in navbar with a pie-chart 56 | // Updates only once every hour or when pool site is visited, showing a pie-chart on pool site 57 | importScriptFile('ab_fl_status.user.js'); 58 | 59 | 60 | // Yen per X and ratio milestones, by Megure, Lemma, NSC, et al. 61 | importScriptFile('ab_yen_stats.user.js'); 62 | 63 | 64 | // Enhanced Torrent View by Megure 65 | // Shows how much yen you would receive if you seeded torrents; 66 | // shows required seeding time; allows sorting and filtering of torrent tables; 67 | // dynamic loading of transfer history tables 68 | importScriptFile('ab_enhanced_torrent_view.user.js'); 69 | 70 | 71 | // Forum search enhancement by Megure 72 | // Load posts into search results; highlight search terms; filter authors; slide through posts 73 | importScriptFile('ab_forum_search_enhancement.user.js'); 74 | 75 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/old/_ab_delicious_template.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes delicious user scripts (updated) 3 | // @author aldy, potatoe, alpha, Megure 4 | // @version 2.1.10 5 | // @description Userscripts to enhance AnimeBytes in various ways. (Updated by TheFallingMan) 6 | // @match https://*.animebytes.tv/* 7 | // @icon http://animebytes.tv/favicon.ico 8 | /* {@grant:16} */ 9 | /* {@require:16} */ 10 | // ==/UserScript== 11 | 12 | (function AnimeBytesDeliciousUserScripts() { 13 | /* {userscripts} */ 14 | })(); 15 | -------------------------------------------------------------------------------- /delicious-userscripts/old/_delicious_common.js: -------------------------------------------------------------------------------- 1 | // Common functions used by many scripts. 2 | // Will be inserted once into the delicious bundle, 3 | // and prepended to each individual userscript. 4 | 5 | // Debug flag. Used to enable/disable some verbose console logging. 6 | var _debug = false; 7 | 8 | // jQuery, just for Pale Moon. 9 | // Note: this doesn't actually import jQuery successfully, 10 | // but for whatever reason, it lets PM load the script. 11 | if ((typeof jQuery) === 'undefined') { 12 | _debug && console.log('setting window.jQuery'); 13 | jQuery = window.jQuery; 14 | $ = window.$; 15 | $j = window.$j; 16 | } 17 | 18 | // Super duper important functions 19 | // Do not delete or something might break and stuff!! :( 20 | HTMLCollection.prototype.each = function (f) { for (var i = 0, e = null; e = this[i]; i++) f.call(e, e); return this; }; 21 | HTMLElement.prototype.clone = function (o) { var n = this.cloneNode(); n.innerHTML = this.innerHTML; if (o !== undefined) for (var e in o) n[e] = o[e]; return n; }; 22 | // Thank firefox for this ugly shit. Holy shit firefox get your fucking shit together >:( 23 | function forEach(arr, fun) { return HTMLCollection.prototype.each.call(arr, fun); } 24 | function clone(ele, obj) { return HTMLElement.prototype.clone.call(ele, obj); } 25 | 26 | function injectScript(content, id) { 27 | var script = document.createElement('script'); 28 | if (id) script.setAttribute('id', id); 29 | script.textContent = content.toString(); 30 | document.body.appendChild(script); 31 | return script; 32 | } 33 | if (typeof GM_getValue === 'undefined' 34 | || (GM_getValue.toString && GM_getValue.toString().indexOf("not supported") > -1)) { 35 | _debug && console.log('Setting fallback localStorage GM_* functions'); 36 | // There is some difference between this.GM_getValue and just GM_getValue. 37 | _debug && console.log(this.GM_getValue); 38 | _debug && typeof GM_getValue !== 'undefined' && console.log(GM_getValue.toString()); 39 | // Previous versions lacked a @grant GM_getValue header, which resulted 40 | // in these being used when they shouldn't have been. 41 | this.GM_getValue = function (key, def) { return localStorage[key] || def; }; 42 | this.GM_setValue = function (key, value) { return localStorage[key] = value; }; 43 | this.GM_deleteValue = function (key) { return delete localStorage[key]; }; 44 | // We set this when we have used localStorage so if in future we switch, 45 | // it will be imported. 46 | GM_setValue('deliciousSettingsImported', 'false'); 47 | _debug && console.log(GM_getValue); 48 | } else { 49 | _debug&& console.log('Using default GM_* functions.'); 50 | // For backwards compatibility, 51 | // we'll implement migrating localStorage to GM settings. 52 | // However, we can't implement the reverse because when localStorage is 53 | // used, GM_getValue isn't even defined so we obviously can't import. 54 | if (GM_getValue('deliciousSettingsImported', 'false') !== 'true') { 55 | _debug && console.log('Importing localStorage to GM settings'); 56 | GM_setValue('deliciousSettingsImported', 'true'); 57 | var keys = Object.keys(localStorage); 58 | keys.forEach(function(key){ 59 | if (GM_getValue(key, 'undefined') === 'undefined') { 60 | _debug && console.log('Imported ' + key); 61 | GM_setValue(key, localStorage[key]); 62 | } else { 63 | _debug && console.log('Key exists ' + key); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | function initGM(gm, def, json, overwrite) { 70 | if (typeof def === "undefined") throw "shit"; 71 | if (typeof overwrite !== "boolean") overwrite = true; 72 | if (typeof json !== "boolean") json = true; 73 | var that = GM_getValue(gm); 74 | if (that != null) { 75 | var err = null; 76 | try { that = ((json) ? JSON.parse(that) : that); } 77 | catch (e) { if (e.message.match(/Unexpected token .*/)) err = e; } 78 | if (!err && Object.prototype.toString.call(that) === Object.prototype.toString.call(def)) { return that; } 79 | else if (overwrite) { 80 | GM_setValue(gm, ((json) ? JSON.stringify(def) : def)); 81 | return def; 82 | } else { if (err) { throw err; } else { return that; } } 83 | } else { 84 | GM_setValue(gm, ((json) ? JSON.stringify(def) : def)); 85 | return def; 86 | } 87 | } -------------------------------------------------------------------------------- /delicious-userscripts/old/ab_dynamic_stylesheets.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB - Dynamic stylesheets 3 | // @author Megure 4 | // @description Changes stylesheets base on time of day. 5 | // @include https://animebytes.tv/* 6 | // @version 0.1 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // ==/UserScript== 11 | 12 | // Dynamic stylesheets by Megure, requires jQuery because I'm lazy 13 | (function DynamicStylesheets() { 14 | 15 | 16 | 17 | function updateSettings() { 18 | var rules = document.querySelectorAll('li.deliciousdynamicstylesheetsrule'); 19 | var result = []; 20 | for (var i = 0; i < rules.length; i++) { 21 | var rule = rules[i]; 22 | var hour = rule.children[0].value; 23 | var stylesheet = rule.children[1].value; 24 | if (hour !== '' && stylesheet !== '') 25 | result.push([parseInt(hour, 10), stylesheet]); 26 | } 27 | result.sort(function (a, b) { return a[0] - b[0]; }); 28 | 29 | GM_setValue('deliciousdynamicstylesheetsrules', JSON.stringify(result)); 30 | } 31 | 32 | function addRule(hour, stylesheet) { 33 | var newLi = document.createElement('li'); 34 | newLi.className = 'deliciousdynamicstylesheetsrule'; 35 | 36 | var hour_input = document.createElement('input'); 37 | hour_input.type = 'number'; 38 | hour_input.min = '0'; 39 | hour_input.max = '23'; 40 | hour_input.step = '1'; 41 | hour_input.placeholder = '0-23'; 42 | hour_input.style.width = '10%'; 43 | hour_input.addEventListener('keyup', updateSettings); 44 | if (typeof hour === 'number') 45 | hour_input.value = hour; 46 | 47 | var stylesheet_input = document.createElement('input'); 48 | stylesheet_input.type = 'text'; 49 | stylesheet_input.placeholder = 'Either a name of an existing stylesheet like Milkyway (case-sensitive), or an external URL like https://aldy.nope.bz/toblerone.css'; 50 | stylesheet_input.style.width = '75%'; 51 | stylesheet_input.addEventListener('keyup', updateSettings); 52 | if (typeof stylesheet === 'string') 53 | stylesheet_input.value = stylesheet; 54 | 55 | var delete_button = document.createElement('button'); 56 | delete_button.textContent = 'Delete rule'; 57 | delete_button.addEventListener('click', function (e) { 58 | e.preventDefault(); 59 | newLi.parentNode.removeChild(newLi); 60 | updateSettings(); 61 | }); 62 | 63 | newLi.appendChild(hour_input); 64 | newLi.appendChild(stylesheet_input); 65 | newLi.appendChild(delete_button); 66 | 67 | var rules = document.querySelectorAll('li.deliciousdynamicstylesheetsrule'); 68 | if (rules.length > 0) { 69 | var lastRule = rules[rules.length - 1]; 70 | lastRule.parentNode.insertBefore(newLi, lastRule.nextSibling); 71 | } 72 | else { 73 | var settings = document.getElementById('deliciousdynamicstylesheets'); 74 | settings.parentNode.parentNode.parentNode.insertBefore(newLi, settings.parentNode.parentNode.nextSibling); 75 | } 76 | } 77 | 78 | function setStylesheet(stylesheet) { 79 | var settings_xhr = new XMLHttpRequest(), settings_dom_parser = new DOMParser(); 80 | settings_xhr.open('GET', "https://animebytes.tv/user.php?action=edit", true); 81 | settings_xhr.send(); 82 | settings_xhr.onreadystatechange = function () { 83 | if (settings_xhr.readyState === 4) { 84 | var settings_document = settings_dom_parser.parseFromString(settings_xhr.responseText, 'text/html'); 85 | var form = settings_document.getElementById('userform'); 86 | 87 | if (form !== null) { 88 | var styleurl = form.querySelector('input#styleurl'); 89 | var stylesheet_select = form.querySelector('select#stylesheet'); 90 | if (styleurl === null || stylesheet_select === null) { 91 | console.log("Could not find style url or stylesheet input on settings page."); 92 | return; 93 | } 94 | var stylesheet_options = settings_document.evaluate('//option[text()="' + stylesheet + '"]', settings_document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 95 | if (stylesheet_options.snapshotItem(0) !== null) { 96 | if (stylesheet_select.value === stylesheet_options.snapshotItem(0).value && styleurl.value === '') { 97 | // Stylesheet settings are already properly set, nothing to do 98 | return; 99 | } 100 | else { 101 | stylesheet_select.setAttribute('onchange', ''); 102 | stylesheet_select.value = stylesheet_options.snapshotItem(0).value; 103 | styleurl.value = ''; 104 | } 105 | } 106 | else { 107 | if (styleurl === stylesheet) { 108 | // Stylesheet settings are already properly set, nothing to do 109 | return; 110 | } 111 | else { 112 | styleurl.value = stylesheet; 113 | } 114 | } 115 | 116 | $.ajax({ 117 | url: "https://animebytes.tv/user.php?action=edit", 118 | type: "post", 119 | data: $(form).serialize() 120 | }); 121 | } 122 | } 123 | } 124 | } 125 | 126 | // Add to user script settings 127 | if (/\/user\.php\?.*action=edit/i.test(document.URL)) { 128 | var settings = document.getElementById('deliciousdynamicstylesheets'); 129 | var add_button = document.createElement('button'); 130 | add_button.textContent = 'Add rule'; 131 | add_button.addEventListener('click', function (e) { 132 | e.preventDefault(); 133 | addRule(); 134 | }); 135 | settings.parentNode.appendChild(add_button); 136 | 137 | // Add existing rules 138 | var rules = JSON.parse(GM_getValue('deliciousdynamicstylesheetsrules', '[]')); 139 | for (var i = 0; i < rules.length; i++) { 140 | var rule = rules[i]; 141 | addRule(rule[0], rule[1]); 142 | } 143 | } 144 | 145 | // Do we have to set the stylesheet? 146 | if (GM_getValue('deliciousdynamicstylesheets', 'false') === 'true') { 147 | var current_hour = (new Date()).getHours(); 148 | var rules = JSON.parse(GM_getValue('deliciousdynamicstylesheetsrules', '[]')); 149 | if (rules.length > 0) { 150 | var result = rules[rules.length - 1][1]; 151 | for (var i = 0; i < rules.length; i++) { 152 | var rule = rules[i]; 153 | if (rule[0] <= current_hour) 154 | result = rule[1]; 155 | } 156 | if (GM_getValue('currentdeliciousdynamicstylesheet', '') !== result) { 157 | setStylesheet(result); 158 | GM_setValue('currentdeliciousdynamicstylesheet', result); 159 | } 160 | } 161 | } 162 | }).call(this); -------------------------------------------------------------------------------- /delicious-userscripts/old/ab_paragraph_spacing.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB Paragraph Spacing 3 | // @namespace TheFallingMan 4 | // @version 0.0.1 5 | // @description Replaces double line spacing with paragraph spacing. 6 | // @author TheFallingMan 7 | // @icon https://animebytes.tv/favicon.ico 8 | // @include https://animebytes.tv/* 9 | // @license GPL-3.0 10 | // ==/UserScript== 11 | 12 | (function ABParagraphSpacing() { 13 | /** 14 | * @type {Array} 15 | */ 16 | var brs = Array.from(document.getElementsByTagName('br')); 17 | for (var i = 0; i < brs.length; i++) { 18 | var br = brs[i]; 19 | var prevBr = br.previousSibling; 20 | if ((prevBr && prevBr.tagName && prevBr.tagName.toUpperCase() === 'BR' && prevBr.style.display !== 'none') 21 | || (prevBr.nodeType === Node.TEXT_NODE && prevBr.nodeValue.slice(-1) === '\n')) { 22 | //console.log(prevBr.tagName.toUpperCase()); 23 | var div = document.createElement('div'); 24 | div.className = 'paragraph-spacer'; 25 | div.style.marginBottom = '8pt'; 26 | 27 | br.parentNode.insertBefore(div, br); 28 | //console.log(div); 29 | if (prevBr.nodeType === Node.TEXT_NODE) { 30 | var spaceLength = /\n*$/.exec(prevBr.nodeValue)[0].length; 31 | var newlineTextNode = prevBr.splitText(prevBr.nodeValue.length - spaceLength); 32 | newlineTextNode.parentNode.removeChild(newlineTextNode); 33 | var newBr = document.createElement('br'); 34 | newBr.style.display = 'none'; 35 | br.parentNode.insertBefore(newBr, div); 36 | } else { 37 | prevBr.style.display = 'none'; 38 | } 39 | br.style.display = 'none'; 40 | } 41 | 42 | } 43 | 44 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/old/assemble_delicious_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import os.path 4 | from datetime import datetime as dt 5 | import shutil 6 | 7 | class ScriptAssembler: 8 | delicious_common = '_delicious_common.js' 9 | def __init__(self, input_file, output_file, out_dir, common_inserted=False, global_indent=''): 10 | self.input_file = input_file 11 | self.out_dir = out_dir 12 | self.output_file = output_file 13 | self.indent_str = '' 14 | self.global_indent = global_indent 15 | self.common_inserted = common_inserted 16 | 17 | def comment(self, comment): 18 | self.output_file.write( 19 | self.global_indent + self.indent_str + 20 | '/* === ' + comment + ' === */\n') 21 | 22 | def insert_extra_script(self, script_name): 23 | self.comment('Inserted from ' + script_name) 24 | with open(script_name, encoding='utf-8') as script: 25 | ScriptAssembler( 26 | script, 27 | self.output_file, 28 | self.out_dir, 29 | self.common_inserted, 30 | self.global_indent+self.indent_str 31 | ).write_script() 32 | self.output_file.write('\n') 33 | self.comment('End ' + script_name) 34 | if (not os.path.basename(script_name).startswith('_')): 35 | # Calls another instance of this class to handle inserting 36 | # common functions into single scripts. 37 | with open(script_name, encoding='utf-8') as script, \ 38 | open(self.out_dir+'/'+script_name, 'w', encoding='utf-8') as new_script_out: 39 | ScriptAssembler( 40 | script, 41 | new_script_out, 42 | self.out_dir 43 | ).write_script() 44 | 45 | def write_script(self): 46 | for line in self.input_file: 47 | trimmed = line.lstrip() 48 | if trimmed: # if trimmed is non-empty 49 | self.indent_str = ' '*(len(line) - len(trimmed)) 50 | if trimmed.startswith('importScriptFile('): 51 | script_name = trimmed.rstrip(');\n')[18:-1] 52 | self.insert_extra_script(script_name) 53 | elif trimmed.startswith('importDeliciousCommon()'): 54 | if not self.common_inserted: 55 | self.common_inserted = True 56 | self.insert_extra_script(self.delicious_common) 57 | else: 58 | self.comment(self.delicious_common+' already inserted.') 59 | else: 60 | #self.output_file.write('\n') 61 | #self.comment('Script generated at ' + dt.now().isoformat()) 62 | self.output_file.write(self.global_indent + line) 63 | 64 | def _main(): 65 | os.chdir(os.path.dirname(__file__)) 66 | with open('_ab_delicious_scripts_loader.user.js', encoding='utf-8') as in_file, \ 67 | open('dist/ab_delicious_scripts.user.js', 'w', encoding='utf-8') as out_file: 68 | assembler = ScriptAssembler( 69 | in_file, 70 | out_file, 71 | 'dist' 72 | ) 73 | assembler.write_script() 74 | 75 | if __name__ == '__main__': 76 | _main() -------------------------------------------------------------------------------- /delicious-userscripts/old/before_library.txt: -------------------------------------------------------------------------------- 1 | { 2 | "ABForumEnhFastSearch": "false", 3 | "ABForumLoadingText": "(Loading)", 4 | "ABForumSearchHideSubfor": "false", 5 | "ABForumSearchHighlightBG": "none", 6 | "ABForumSearchHighlightFG": "none", 7 | "ABForumSearchWorkInFS": "false", 8 | "ABForumToggleText": "(Toggle)", 9 | "ABRowSetting": "[{\"href\":\"google.com\",\"text\":\"Goodle\"}]", 10 | "ABTorrentsShowYen": "true", 11 | "FLPoolAmounts": "[5000000,2500000,2500000,2500000,2500000,2500000,2000000,1754424,1035398,1024000,2556610,74129568]", 12 | "FLPoolColors": "[\"red\",\"red\",\"red\",\"red\",\"red\",\"red\",\"red\",\"red\",\"red\",\"red\",\"lightgrey\",\"black\"]", 13 | "FLPoolContribution": 259, 14 | "FLPoolCurrent": 25870432, 15 | "FLPoolHrefs": "[\"https://animebytes.tv/user/profile/Asandari\",\"https://animebytes.tv/user/profile/koisama\",\"https://animebytes.tv/user/profile/violet\",\"https://animebytes.tv/user/profile/chifuyuneee\",\"https://animebytes.tv/user/profile/Ankular\",\"https://animebytes.tv/user/profile/nefan\",\"https://animebytes.tv/user/profile/Goalie\",\"https://animebytes.tv/user/profile/Dnias\",\"https://animebytes.tv/user/profile/caitsith2\",\"https://animebytes.tv/user/profile/theblackknight\",\"https://animebytes.tv/konbini/pool\",\"https://animebytes.tv/konbini/pool\"]", 16 | "FLPoolLastUpdate": 1531040150439, 17 | "FLPoolMax": 100000000, 18 | "FLPoolTitles": "[\"Asandari\",\"koisama\",\"violet\",\"chifuyuneee\",\"Ankular\",\"nefan\",\"Goalie\",\"Dnias\",\"caitsith2\",\"theblackknight\",\"Other\",\"Missing\"]", 19 | "creation": "1498649580000", 20 | "currentdeliciousdynamicstylesheet": "Tentacletastic", 21 | "deliciousSettingsImported": "true", 22 | "deliciousdynamicstylesheets": "true", 23 | "deliciousdynamicstylesheetsrules": "[[1,\"Tentacletastic\"],[23,\"Coalbytes\"]]", 24 | "deliciousflpoolposition": "after #userinfo_minor", 25 | "deliciousfreeleechpool": "true", 26 | "delicioushyperquote": "false", 27 | "deliciouskeyboard": "true", 28 | "deliciousquote": "false", 29 | "deliciousratio": "true", 30 | "delicioustitleflip": "true", 31 | "delicioustitlenotifications": "false", 32 | "delicioustreats": "false", 33 | "deliciousyenperx": "false", 34 | "delicousnavbarpiechart": "true", 35 | "userscript-highlight-installinfo": "yay" 36 | } -------------------------------------------------------------------------------- /delicious-userscripts/old/concat_userscripts.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import glob 4 | import pathlib 5 | 6 | class Concat: 7 | _placeholder_re = re.compile(r'^(\s*)/\* \{([^:}]+)(?::(\d+))?\} \*/\n$') 8 | 9 | _grant_re = re.compile(r'^//\s*@grant\s+(.+)$') 10 | _require_re = re.compile(r'^//\s*@require\s+(.+)$') 11 | 12 | def __init__(self, template_file): 13 | self._template_file = template_file 14 | self._script_lines = [] 15 | self._grants = [] 16 | self._requires = [] 17 | 18 | def write_bundle(self, out_file, userscripts): 19 | for f in userscripts: 20 | print('Reading script', f) 21 | p = pathlib.PurePath(f).as_posix() 22 | self._script_lines.append('/* Begin ' + p + ' */\n') 23 | 24 | with open(f, 'r', encoding='utf-8') as script_file: 25 | for l in script_file: 26 | self._script_lines.append(l) 27 | 28 | grant_match = self._grant_re.match(l) 29 | if grant_match and grant_match[1] not in self._grants: 30 | print('Found @grant', grant_match[1]) 31 | self._grants.append(grant_match[1]) 32 | 33 | require_match = self._require_re.match(l) 34 | if require_match and require_match[1] not in self._requires: 35 | print('Found @require', require_match[1]) 36 | self._requires.append(require_match[1]) 37 | if not self._script_lines[len(self._script_lines)-1].endswith('\n'): 38 | self._script_lines[len(self._script_lines)-1] += '\n' 39 | self._script_lines.append('/* End ' + p + ' */\n\n\n') 40 | 41 | print('Writing bundle script') 42 | with open(out_file, 'w', encoding='utf-8') as out: 43 | self._out_file_obj = out 44 | with open(self._template_file, 'r', encoding='utf-8') as template: 45 | for template_line in template: 46 | match = self._placeholder_re.match(template_line) 47 | if match: 48 | if match[2] == '@require': 49 | print('Inserting @require') 50 | self._write_sublines(self._requires, 51 | '// @require', int(match[3]), match[1], '\n') 52 | elif match[2] == '@grant': 53 | print('Inserting @grant') 54 | self._write_sublines(self._grants, 55 | '// @grant', int(match[3]), match[1], '\n') 56 | elif match[2] == 'userscripts': 57 | print('Inserting userscripts') 58 | self._write_sublines(self._script_lines, 59 | '', 0, match[1]) 60 | else: 61 | out.write(template_line) 62 | 63 | 64 | def _write_sublines(self, lines, prefix, margin, indent, suffix=''): 65 | for l in lines: 66 | self._out_file_obj.write(indent + prefix.ljust(margin) + l + suffix) 67 | 68 | def _main(): 69 | os.chdir(os.path.dirname(__file__)) 70 | Concat('./_ab_delicious_template.js').write_bundle( 71 | './dist/ab_delicious_scripts.user.js', 72 | sorted(glob.glob('./src/*.user.js'))) 73 | 74 | if __name__ == '__main__': 75 | _main() -------------------------------------------------------------------------------- /delicious-userscripts/old/create_dev_userscript.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import glob 4 | 5 | if __name__ == '__main__': 6 | os.chdir(os.path.dirname(__file__)) 7 | with open('./dist/ab_delicious_scripts.dev.user.js', 'w', encoding='utf-8') as f: 8 | f.write('''// ==UserScript== 9 | // @name AnimeBytes Delicious Bundle (BOOTSTRAP) 10 | // @grant GM_getValue 11 | // @grant GM_setValue 12 | // @grant GM_deleteValue 13 | // @match https://*.animebytes.tv/* 14 | // @icon http://animebytes.tv/favicon.ico\n''') 15 | requires = ['./../delicious-library/src/ab_delicious_library.js'] 16 | requires.extend(glob.glob('./src/*.user.js')) 17 | for x in requires: 18 | f.write('// @require '+pathlib.Path(x).resolve().as_uri() + '\n') 19 | f.write('''// ==/UserScript== 20 | 21 | (function() { 22 | console.info('Delicious bundle developer version loaded.'); 23 | })();''') 24 | 25 | with open('./dist/ab_delicious_scripts.auto.user.js', 'w', encoding='utf-8') as f: 26 | f.write('''// ==UserScript== 27 | // @name AnimeBytes Delicious Bundle (Automatic) 28 | // @version 1.0.1 29 | // @grant GM_getValue 30 | // @grant GM_setValue 31 | // @grant GM_deleteValue 32 | // @match https://*.animebytes.tv/* 33 | // @icon http://animebytes.tv/favicon.ico 34 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js\n''') 35 | requires = [] 36 | requires.extend(glob.glob('./src/*.user.js')) 37 | for x in requires: 38 | prefix = 'https://github.com/momentary0/AB-Userscripts/raw/master/delicious-userscripts' 39 | f.write('// @require '+prefix+x[1:].replace('\\', '/') + '\n') 40 | f.write('''// ==/UserScript== 41 | 42 | (function() { 43 | console.info('Delicious bundle automatic version loaded.'); 44 | })();''') -------------------------------------------------------------------------------- /delicious-userscripts/old/localStorage_keys.txt: -------------------------------------------------------------------------------- 1 | // All storage keys used by delicious, in an array-like format. 2 | // See further down for code. 3 | 4 | var keys = [ 5 | "ABForumEnhFastSearch", 6 | "ABForumEnhWorkInRest", 7 | "ABForumLoadingText", 8 | "ABForumLoadText", 9 | "ABForumSearchHideSubfor", 10 | "ABForumSearchHighlightBG", 11 | "ABForumSearchHighlightFG", 12 | "ABForumSearchWorkInFS", 13 | "ABForumToggleText", 14 | "ABHistDynLoad", 15 | "ABSortTorrents", 16 | "ABTorrentsFilter", 17 | "ABTorrentsReqTime", 18 | "ABTorrentsShowYen", 19 | "ABTorrentsYenTimeFrame", 20 | "creation", 21 | "currentdeliciousdynamicstylesheet", 22 | "deliciousdynamicstylesheets", 23 | "deliciousdynamicstylesheetsrules", 24 | "deliciousflpoolposition", 25 | "deliciousfreeleechpool", 26 | "delicioushyperquote", 27 | "deliciouskeyboard", 28 | "deliciousquote", 29 | "deliciousratio", 30 | "deliciousSettingsImported", 31 | "delicioustitleflip", 32 | "delicioustitlenotifications", 33 | "delicioustreats", 34 | "deliciousyenperx", 35 | "delicousnavbarpiechart", 36 | "FLPoolAmounts", 37 | "FLPoolColors", 38 | "FLPoolContribution", 39 | "FLPoolCurrent", 40 | "FLPoolHrefs", 41 | "FLPoolLastUpdate", 42 | "FLPoolMax", 43 | "FLPoolTitles" 44 | ] 45 | 46 | // Code to delete all such keys. 47 | for (var i = 0; i < keys.length; i++) { 48 | localStorage.removeItem(keys[i]); 49 | } -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_fl_status.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB - Freeleech Pool Status 3 | // @author Megure (inspired by Lemma, Alpha, NSC) 4 | // @description Shows current freeleech pool status in navbar with a pie-chart 5 | // @include https://animebytes.tv/* 6 | // @version 0.1.1.3 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_getValue 9 | // @grant GM_setValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | // ==/UserScript== 12 | 13 | // Freeleech Pool Status by Megure, inspired by Lemma, Alpha, NSC 14 | // Shows current freeleech pool status in navbar with a pie-chart 15 | // Updates only once every hour or when pool site is visited, showing a pie-chart on pool site 16 | (function ABFLStatus() { 17 | delicious.settings._migrateStringSetting('deliciousflpoolposition'); 18 | 19 | delicious.settings.init('deliciousflpoolposition', 'after #userinfo_minor'); 20 | delicious.settings.init('deliciousfreeleechpool', true); 21 | delicious.settings.init('deliciousnavbarpiechart', true); 22 | var pieLocations = delicious.settings.init('deliciousflpiepositions', { 23 | 'navbar': delicious.settings.get('deliciousnavbarpiechart'), 24 | 'profile': delicious.settings.get('deliciousnavbarpiechart') 25 | }); 26 | 27 | if (delicious.settings.ensureSettingsInserted()) { 28 | var section = delicious.settings.createCollapsibleSection('Delicious Freeleech Pool'); 29 | var s = section.querySelector('.settings_section_body'); 30 | s.appendChild(delicious.settings.createCheckbox( 31 | 'deliciousfreeleechpool', 32 | 'Enable/Disable', 33 | 'Shows current freeleech pool progress in the navbar and on user pages' 34 | + ' (updated once an hour or when freeleech pool site is visited).')); 35 | s.appendChild(delicious.settings.createDropDown( 36 | 'deliciousflpoolposition', 37 | 'Navbar Position', 38 | 'Select position of freeleech pool progress in the navbar or disable it.', 39 | [['Before user info', 'before #userinfo_minor'], 40 | ['After user info', 'after #userinfo_minor'], 41 | ['Before menu', 'before .main-menu.nobullet'], 42 | ['After menu', 'after .main-menu.nobullet'], 43 | ['Don\'t display', 'none']], 44 | {default: 'after #userinfo_minor'} 45 | )); 46 | s.appendChild(delicious.settings.createFieldSetSetting( 47 | 'deliciousflpiepositions', 48 | 'FL Pie Chart Locations', 49 | [['Navbar dropdown', 'navbar'], ['User profile', 'profile']] 50 | )); 51 | delicious.settings.insertSection(section); 52 | } 53 | 54 | if (!delicious.settings.get('deliciousfreeleechpool')) 55 | return; 56 | 57 | function niceNumber(num) { 58 | var res = ''; 59 | while (num >= 1000) { 60 | res = ',' + ('00' + (num % 1000)).slice(-3) + res; 61 | num = Math.floor(num / 1000); 62 | } 63 | return num + res; 64 | } 65 | var locked = false; 66 | function getFLInfo() { 67 | function parseFLInfo(elem) { 68 | console.log("Parsing FL info from element: ", elem); 69 | var boxes = elem.querySelectorAll('#content .box.pad'); 70 | //console.log(boxes); 71 | if (boxes.length < 3) { 72 | console.error("Not enough boxes."); 73 | return; 74 | } 75 | 76 | // The first box holds the current amount, the max amount and the user's individual all-time contribution 77 | var match = boxes[0].textContent.match(/have ¥([0-9,]+) \/ ¥([0-9,]+)/i), 78 | max = parseInt(GM_getValue('FLPoolMax', '50000000'), 10), 79 | current = parseInt(GM_getValue('FLPoolCurrent', '0'), 10); 80 | if (match == null) { 81 | // Updated 2018-02-23 according to oregano's suggestion 82 | match = boxes[0].textContent.match(/You must wait for freeleech to be over before donating/i); 83 | if (match != null) current = max; 84 | } 85 | else { 86 | current = parseInt(match[1].replace(/,/g, ''), 10); 87 | max = parseInt(match[2].replace(/,/g, ''), 10); 88 | } 89 | if (match != null) { 90 | GM_setValue('FLPoolCurrent', current); 91 | GM_setValue('FLPoolMax', max); 92 | } 93 | // Check first box for user's individual all-time contribution 94 | match = boxes[0].textContent.match(/you've donated ¥([0-9,]+)/i); 95 | if (match != null) 96 | GM_setValue('FLPoolContribution', parseInt(match[1].replace(/,/g, ''), 10)); 97 | 98 | // The third box holds the top 10 donators for the current box 99 | var box = boxes[2], 100 | firstP = box.querySelector('p'), 101 | tr = box.querySelector('table').querySelectorAll('tbody > tr'); 102 | 103 | var titles = [], hrefs = [], amounts = [], colors = [], sum = 0; 104 | for (var i = 0; i < tr.length; i++) { 105 | var el = tr[i], 106 | td = el.querySelectorAll('td'); 107 | 108 | titles[i] = td[0].textContent; 109 | hrefs[i] = td[0].querySelector('a').href; 110 | amounts[i] = parseInt(td[1].textContent.replace(/[,¥]/g, ''), 10); 111 | colors[i] = 'red'; 112 | sum += amounts[i]; 113 | } 114 | 115 | // Updated 2018-02-23. Properly draw full pie when FL active. 116 | if (current === max && sum === 0) { 117 | titles[0] = "Freeleech!"; 118 | hrefs[0] = 'https://animebytes.tv/konbini/pool'; 119 | amounts[0] = current; 120 | colors[0] = 'red'; 121 | sum = current; 122 | } 123 | else { 124 | // Also add others and missing to the arrays 125 | // 2018-02-23 But only if FL isn't active. 126 | next_index = titles.length; 127 | titles[next_index] = 'Other'; 128 | hrefs[next_index] = 'https://animebytes.tv/konbini/pool'; 129 | amounts[next_index] = current - sum; 130 | colors[next_index] = 'lightgrey'; 131 | 132 | titles[next_index + 1] = 'Missing'; 133 | hrefs[next_index + 1] = 'https://animebytes.tv/konbini/pool'; 134 | amounts[next_index + 1] = max - current; 135 | colors[next_index + 1] = 'black'; 136 | } 137 | 138 | GM_setValue('FLPoolLastUpdate', Date.now()); 139 | GM_setValue('FLPoolTitles', JSON.stringify(titles)); 140 | GM_setValue('FLPoolHrefs', JSON.stringify(hrefs)); 141 | GM_setValue('FLPoolAmounts', JSON.stringify(amounts)); 142 | GM_setValue('FLPoolColors', JSON.stringify(colors)); 143 | } 144 | 145 | // Either parse document or retrieve freeleech pool site 60*60*1000 ms after last retrieval 146 | if (/konbini\/pool$/i.test(document.URL)) 147 | parseFLInfo(document); 148 | else if (Date.now() - parseInt(GM_getValue('FLPoolLastUpdate', '0'), 10) > 3600000 && locked === false) { 149 | locked = true; 150 | console.log("Attempting to update FL status..."); 151 | // Fix suggested by https://animebytes.tv/user/profile/oregano 152 | // https://discourse.mozilla.org/t/webextension-xmlhttprequest-issues-no-cookies-or-referrer-solved/11224/18 153 | try { 154 | var xhr = XPCNativeWrapper(new window.wrappedJSObject.XMLHttpRequest()); 155 | } catch (exc) { 156 | var xhr = new XMLHttpRequest(); 157 | } 158 | parser = new DOMParser(); 159 | xhr.open('GET', "https://animebytes.tv/konbini/pool", true); 160 | xhr.send(); 161 | xhr.onreadystatechange = function () { 162 | console.log("FL xhr.onreadystatechange: ", xhr.readyState); 163 | if (xhr.readyState === 4) { 164 | parseFLInfo(parser.parseFromString(xhr.responseText, 'text/html')); 165 | updatePieChart(); 166 | locked = false; 167 | } 168 | }; 169 | } 170 | } 171 | 172 | function getPieChart() { 173 | function circlePart(diff, title, href, color) { 174 | if (diff == 0) return ''; 175 | var x = Math.sin(phi), y = Math.cos(phi); 176 | phi -= 2 * Math.PI * diff / max; 177 | var v = Math.sin(phi), w = Math.cos(phi); 178 | var z = 0; 179 | if (2 * diff > max) 180 | z = 1; // use long arc 181 | var perc = (100 * diff / max).toFixed(1) + '%\n' + niceNumber(diff) + ' ¥'; 182 | 183 | // 2018-02-23 Hardcoded since rounding errors were making the pie a thin strip when it was a single 184 | // slice at 100%. 185 | if (diff === max) { 186 | /*v = -6.283185273215512e-8; 187 | w = -0.999999999999998; 188 | z = 1; 189 | x = 1.2246467991473532e-16; 190 | y = -1;*/ 191 | v = -0.000001; 192 | w = -1; 193 | z = 1; 194 | x = 0; 195 | y = -1; 196 | 197 | } 198 | return '\n' + 200 | 201 | '\n' + 202 | '\n' + 203 | '\n\n'; 204 | } 205 | 206 | var str = '' + 207 | 'Most Donated To This Box Pie-Chart'; 208 | try { 209 | var phi = Math.PI, max = parseInt(GM_getValue('FLPoolMax', '50000000'), 10), 210 | titles = JSON.parse(GM_getValue('FLPoolTitles', '[]')), 211 | hrefs = JSON.parse(GM_getValue('FLPoolHrefs', '[]')), 212 | amounts = JSON.parse(GM_getValue('FLPoolAmounts', '[]')), 213 | colors = JSON.parse(GM_getValue('FLPoolColors', '[]')); 214 | for (var i = 0; i < titles.length; i++) { 215 | str += circlePart(amounts[i], titles[i], hrefs[i], colors[i]); 216 | } 217 | } catch (e) { 218 | console.error("Error rendering FL pie chart.", e); 219 | } 220 | return str + ''; 221 | } 222 | 223 | function updatePieChart() { 224 | var pieChart = getPieChart(); 225 | p.innerHTML = pieChart; 226 | p3.innerHTML = pieChart; 227 | if (pieLocations['navbar']) { 228 | li.innerHTML = pieChart; 229 | } 230 | p2.innerHTML = 'We currently have ¥' + niceNumber(parseInt(GM_getValue('FLPoolCurrent', '0'), 10)) + ' / ¥' + niceNumber(parseInt(GM_getValue('FLPoolMax', '50000000'), 10)) + ' in our donation box.
'; 231 | p2.innerHTML += '(That means we\'re ¥' + niceNumber(parseInt(GM_getValue('FLPoolMax', '50000000'), 10) - parseInt(GM_getValue('FLPoolCurrent', '0'), 10)) + ' away from sitewide freeleech!)
'; 232 | p2.innerHTML += 'In total, you\'ve donated ¥' + niceNumber(parseInt(GM_getValue('FLPoolContribution', '0'), 10)) + ' to the freeleech pool.
'; 233 | p2.innerHTML += 'Last updated ' + Math.round((Date.now() - parseInt(GM_getValue('FLPoolLastUpdate', Date.now()), 10)) / 60000) + ' minutes ago.'; 234 | a.textContent = 'FL: ' + (100 * parseInt(GM_getValue('FLPoolCurrent', '0'), 10) / parseInt(GM_getValue('FLPoolMax', '50000000'), 10)).toFixed(1) + '%'; 235 | nav.replaceChild(a, nav.firstChild); 236 | } 237 | 238 | var pos = delicious.settings.get('deliciousflpoolposition'); 239 | 240 | if (pos !== 'none' || /user\.php\?id=/i.test(document.URL) || /konbini\/pool/i.test(document.URL)) { 241 | var p = document.createElement('p'), 242 | p2 = document.createElement('center'), 243 | p3 = document.createElement('p'), 244 | nav = document.createElement('li'), 245 | a = document.createElement('a'), 246 | ul = document.createElement('ul'), 247 | li = document.createElement('li'); 248 | console.log("Inserting FL status. pos: ", pos); 249 | a.href = '/konbini/pool'; 250 | nav.appendChild(a); 251 | if (pieLocations['navbar']) { 252 | var outerSpan = document.createElement('span'); 253 | outerSpan.className += "dropit hover clickmenu"; 254 | outerSpan.addEventListener('click', delicious.utilities.toggleSubnav); 255 | outerSpan.innerHTML += ''; 256 | 257 | // nav is the li.navmenu 258 | nav.appendChild(outerSpan); 259 | 260 | // this ul contains the pie (somehow) 261 | ul.appendChild(li); 262 | ul.className = 'subnav nobullet'; 263 | ul.style.display = 'none'; 264 | nav.appendChild(ul); 265 | nav.className = 'navmenu'; 266 | nav.id = "fl_menu"; 267 | } 268 | if (pos !== 'none') { 269 | pos = pos.split(' '); 270 | var parent = document.querySelector(pos[1]); 271 | if (parent !== null) { 272 | getFLInfo(); 273 | if (pos[0] === 'after') 274 | parent.appendChild(nav); 275 | if (pos[0] === 'before') 276 | parent.insertBefore(nav, parent.firstChild); 277 | } 278 | } 279 | 280 | updatePieChart(); 281 | 282 | if (pieLocations['profile'] && /user\.php\?id=/i.test(document.URL)) { 283 | var userstats = document.querySelector('#user_rightcol > .box'); 284 | if (userstats != null) { 285 | var tw = document.createTreeWalker(userstats, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /Yen per day/i.test(node.data); } }); 286 | if (tw.nextNode() != null) { 287 | getFLInfo(); 288 | var cNode = document.querySelector('.userstatsleft'); 289 | var hr = document.createElement('hr'); 290 | hr.style.clear = 'both'; 291 | cNode.insertBefore(hr, cNode.lastElementChild); 292 | cNode.insertBefore(p2, cNode.lastElementChild); 293 | cNode.insertBefore(p3, cNode.lastElementChild); 294 | } 295 | } 296 | } 297 | 298 | if (/konbini\/pool/i.test(document.URL)) { 299 | var tw = document.createTreeWalker(document.getElementById('content'), NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^\s*Most Donated to This Box\s*$/i.test(node.data); } }); 300 | if (tw.nextNode() !== null) { 301 | tw.currentNode.parentNode.insertBefore(p, tw.currentNode.nextSibling); 302 | } 303 | } 304 | } 305 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_hide_treats.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB - Hide treats 3 | // @author Alpha 4 | // @description Hide treats on profile. 5 | // @include https://animebytes.tv/* 6 | // @version 0.1.1 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | // ==/UserScript== 12 | 13 | // Hide treats by Alpha 14 | // Hide treats on profile. 15 | (function ABHideTreats(){ 16 | var _enabled = delicious.settings.basicScriptCheckbox( 17 | 'delicioustreats', 18 | 'Disgusting Treats', 19 | 'Hide those hideous treats on profile pages!' 20 | ); 21 | if (!_enabled) 22 | return; 23 | 24 | var treatsnode = document.evaluate('//*[@id="user_leftcol"]/div[@class="box" and div[@class="head" and .="Treats"]]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 25 | if (treatsnode) treatsnode.style.display = "none"; 26 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_keyboard_shortcuts.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes - Forum Keyboard Shortcuts 3 | // @author Alpha, modified by Megure 4 | // @description Enables keyboard shortcuts for forum (new post and edit) and PM 5 | // @include https://animebytes.tv/* 6 | // @version 0.1.2 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | // ==/UserScript== 12 | 13 | 14 | // Keyboard shortcuts by Alpha, mod by Megure 15 | // Enables keyboard shortcuts for forum (new post and edit) and PM 16 | (function ABKeyboardShortcuts() { 17 | var _debug = false; 18 | 19 | var _enabled = delicious.settings.basicScriptCheckbox( 20 | 'deliciouskeyboard', 21 | 'Delicious Keyboard Shortcuts', 22 | 'Keyboard shortcuts to make typing BBCode easier.' 23 | ); 24 | if (!_enabled) 25 | return; 26 | if (document.querySelector('textarea') === null) 27 | return; 28 | 29 | function custom_insert_text(open, close) { 30 | var elem = document.activeElement; 31 | if (elem.selectionStart || elem.selectionStart == '0') { 32 | var startPos = elem.selectionStart; 33 | var endPos = elem.selectionEnd; 34 | elem.value = elem.value.substring(0, startPos) + open + elem.value.substring(startPos, endPos) + close + elem.value.substring(endPos, elem.value.length); 35 | elem.selectionStart = elem.selectionEnd = endPos + open.length + close.length; 36 | elem.focus(); 37 | if (close.length == 0) 38 | elem.setSelectionRange(startPos + open.length, startPos + open.length); 39 | else 40 | elem.setSelectionRange(startPos + open.length, endPos + open.length); 41 | } else if (document.selection && document.selection.createRange) { 42 | elem.focus(); 43 | var sel = document.selection.createRange(); 44 | sel.text = open + sel.text + close; 45 | if (close.length != 0) { 46 | sel.move("character", -close.length); 47 | sel.select(); 48 | } 49 | elem.focus(); 50 | } else { 51 | elem.value += open; 52 | elem.focus(); 53 | elem.value += close; 54 | } 55 | } 56 | 57 | var ctrlorcmd = (navigator.appVersion.indexOf('Mac') != -1) ? '⌘' : 'Ctrl'; 58 | var insertedQueries = []; 59 | 60 | function insert(e, key, ctrl, alt, shift, open, close, query) { 61 | /* Function to handle detecting key combinations and inserting the 62 | shortcut text onto the relevent buttons. */ 63 | if (false) { 64 | //console.log(String.fromCharCode((96 <= key && key <= 105)? key-48 : key)); 65 | console.log(String.fromCharCode(e.charCode)); 66 | console.log(e.ctrlKey); 67 | console.log(e.metaKey); 68 | console.log(e.altKey); 69 | console.log(e.shiftKey); 70 | } 71 | // Javascript has some discrepancies with symbols and their keycodes. 72 | var keyCode; 73 | switch (key) { 74 | case '.': 75 | keyCode = 190; 76 | break; 77 | case '/': 78 | keyCode = 191; 79 | break; 80 | default: 81 | keyCode = key.charCodeAt(0); 82 | } 83 | 84 | // Checks if correct modifiers are pressed 85 | if (document.activeElement.tagName.toLowerCase() === 'textarea' && 86 | (ctrl === (e.ctrlKey || e.metaKey)) && 87 | (alt === e.altKey) && 88 | (shift === e.shiftKey) && 89 | (e.keyCode === keyCode)) { 90 | e.preventDefault(); 91 | custom_insert_text(open, close); 92 | return false; 93 | } 94 | 95 | if (query !== undefined) { 96 | if (insertedQueries.indexOf(query) === -1) { 97 | insertedQueries.push(query); 98 | var imgs = document.querySelectorAll(query); 99 | for (var i = 0; i < imgs.length; i++) { 100 | var img = imgs[i]; 101 | img.title += ' ('; 102 | if (ctrl) img.title += ctrlorcmd + '+'; 103 | if (alt) img.title += 'Alt+'; 104 | if (shift) img.title += 'Shift+'; 105 | img.title += key + ')'; 106 | } 107 | } 108 | } 109 | } 110 | 111 | function keydownHandler(e) { 112 | // Used as a keydown event handler. 113 | // Defines all keyboard shortcuts. 114 | /** 115 | * All keyboard shortcuts based on MS Word 116 | **/ 117 | // Bold 118 | insert(e, 'B', true, false, false, '[b]', '[/b]', '#bbcode img[title="Bold"]'); 119 | // Italics 120 | insert(e, 'I', true, false, false, '[i]', '[/i]', '#bbcode img[title="Italics"]'); 121 | // Underline 122 | insert(e, 'U', true, false, false, '[u]', '[/u]', '#bbcode img[title="Underline"]'); 123 | // Align right 124 | insert(e, 'R', true, false, false, '[align=right]', '[/align]'); 125 | // Align left 126 | insert(e, 'L', true, false, false, '[align=left]', '[/align]'); 127 | // Align center 128 | insert(e, 'E', true, false, false, '[align=center]', '[/align]'); 129 | // Spoiler 130 | insert(e, 'S', true, false, false, '[spoiler]', '[/spoiler]', '#bbcode img[title="Spoilers"]'); 131 | // Hide 132 | insert(e, 'H', true, false, false, '[hide]', '[/hide]', '#bbcode img[title="Hide"]'); 133 | // YouTube 134 | insert(e, 'Y', true, true, false, '[youtube]', '[/youtube]', '#bbcode img[alt="YouTube"]'); 135 | // Image 136 | insert(e, 'G', true, false, false, '[img]', '[/img]', '#bbcode img[title="Image"]'); 137 | // Bullet point and numbered list 138 | insert(e, '.', true, false, false, '[*] ', '', '#bbcode img[title="Unordered list"]'); 139 | insert(e, '/', true, false, false, '[#] ', '', '#bbcode img[title="Ordered list"]'); 140 | // URL 141 | insert(e, 'K', true, false, false, '[url=]', '[/url]', '#bbcode img[title="URL"]'); 142 | } 143 | 144 | var textAreas = document.querySelectorAll('textarea'); 145 | // inserts shortcuts into title text on load, rather than 146 | // doing it when first key is pressed. 147 | keydownHandler({}); 148 | for (var i = 0; i < textAreas.length; i++) { 149 | textAreas[i].addEventListener('keydown', keydownHandler, false); 150 | } 151 | 152 | function mutationHandler(mutations, observer) { 153 | _debug && console.log(mutations); 154 | if (mutations[0].addedNodes.length) { 155 | var textAreas = document.querySelectorAll('textarea'); 156 | for (var i = 0; i < textAreas.length; i++) { 157 | textAreas[i].addEventListener('keydown', keydownHandler, false); 158 | } 159 | } 160 | } 161 | 162 | // Watch for new textareas (e.g. forum edit post) 163 | var mutationObserver = new MutationObserver(mutationHandler); 164 | mutationObserver.observe(document.querySelector('body'), { childList: true, subtree: true }); 165 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_title_inverter.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes forums title inverter 3 | // @author potatoe 4 | // @version 0.1.1 5 | // @description Inverts the forums titles. 6 | // @icon https://animebytes.tv/favicon.ico 7 | // @include https://animebytes.tv/forums.php?* 8 | // @match https://animebytes.tv/forums.php?* 9 | // @grant GM_setValue 10 | // @grant GM_getValue 11 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 12 | // ==/UserScript== 13 | 14 | // Forums title inverter by Potatoe 15 | // Inverts the forums titles. 16 | (function ABTitleInverter() { 17 | var _enabled = delicious.settings.basicScriptCheckbox( 18 | 'delicioustitleflip', 19 | 'Delicious Title Flip', 20 | 'Flips the tab title.' 21 | ); 22 | if (!_enabled) 23 | return; 24 | 25 | if (document.title.indexOf(' > ') !== -1) { 26 | document.title = document.title.split(" :: ")[0].split(" > ").reverse().join(" < ") + " :: AnimeBytes"; 27 | } 28 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_title_notifications.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AnimeBytes - Title Notifications 3 | // @author Megure 4 | // @description Will prepend the number of notifications to the title 5 | // @include https://animebytes.tv/* 6 | // @version 0.1.1 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | // ==/UserScript== 12 | 13 | // Title Notifications by Megure 14 | // Will prepend the number of notifications to the title 15 | (function ABTitleNotifications() { 16 | var _enabled = delicious.settings.basicScriptCheckbox( 17 | 'delicioustitlenotifications', 18 | 'Delicious Title Notifications', 19 | 'Display number of notifications in the tab title.' 20 | ); 21 | if (!_enabled) 22 | return; 23 | 24 | var new_count = 0, _i, cnt, notifications = document.querySelectorAll('#alerts .new_count'), _len = notifications.length; 25 | for (_i = 0; _i < _len; _i++) { 26 | cnt = parseInt(notifications[_i].textContent, 10); 27 | if (!isNaN(cnt)) 28 | new_count += cnt; 29 | } 30 | if (new_count > 0) 31 | document.title = '(' + new_count + ') ' + document.title; 32 | })(); -------------------------------------------------------------------------------- /delicious-userscripts/src/ab_yen_stats.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB - Yen per X and ratio milestones 3 | // @author Megure, Lemma, NSC, et al. 4 | // @description Yen per X and ratio milestones, by Megure, Lemma, NSC, et al. 5 | // @include https://animebytes.tv/user.php* 6 | // @version 0.1.2 7 | // @icon http://animebytes.tv/favicon.ico 8 | // @grant GM_setValue 9 | // @grant GM_getValue 10 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 11 | // ==/UserScript== 12 | 13 | // Yen per X and ratio milestones, by Megure, Lemma, NSC, et al. 14 | (function ABYenStats() { 15 | delicious.settings.basicScriptCheckbox('deliciousyenperx', 'Delicious Yen Per X', 16 | 'Shows how much yen you receive per X and as upload equivalent.'); 17 | delicious.settings.basicScriptCheckbox('deliciousratio', 'Delicious Ratio', 18 | 'Shows ratio, raw ratio and how much upload/download you need for certain ratio milestones.'); 19 | 20 | var _debug = false; 21 | 22 | if (!/user\.php\?id=/i.test(document.URL)) 23 | return; 24 | 25 | function compoundInterest(years) { 26 | return (Math.pow(2, years) - 1) / Math.log(2); 27 | } 28 | function formatInteger(num) { 29 | var res = ''; 30 | while (num >= 1000) { 31 | res = ',' + ('00' + (num % 1000)).slice(-3) + res; 32 | num = Math.floor(num / 1000); 33 | } 34 | return '¥' + num + res; 35 | } 36 | function bytecount(num, unit) { 37 | // For whatever reason, this was always called with .toUpperCase() 38 | // by the original author, but newer KiB style prefixes have 39 | // a lowercase. Keeping both for compatibility. 40 | switch (unit) { 41 | case 'B': 42 | return num * Math.pow(1024, 0); 43 | case 'KiB': 44 | case 'KIB': 45 | return num * Math.pow(1024, 1); 46 | case 'MiB': 47 | case 'MIB': 48 | return num * Math.pow(1024, 2); 49 | case 'GiB': 50 | case 'GIB': 51 | return num * Math.pow(1024, 3); 52 | case 'TiB': 53 | case 'TIB': 54 | return num * Math.pow(1024, 4); 55 | case 'PiB': 56 | case 'PIB': 57 | return num * Math.pow(1024, 5); 58 | case 'EiB': 59 | case 'EIB': 60 | return num * Math.pow(1024, 6); 61 | } 62 | } 63 | function humancount(num) { 64 | if (num == 0) return '0 B'; 65 | var i = Math.floor(Math.log(Math.abs(num)) / Math.log(1024)); 66 | num = (num / Math.pow(1024, i)).toFixed(2); 67 | switch (i) { 68 | case 0: 69 | return num + ' B'; 70 | case 1: 71 | return num + ' KiB'; 72 | case 2: 73 | return num + ' MiB'; 74 | case 3: 75 | return num + ' GiB'; 76 | case 4: 77 | return num + ' TiB'; 78 | case 5: 79 | return num + ' PiB'; 80 | case 6: 81 | return num + ' EiB'; 82 | default: 83 | return num + ' × 1024^' + i + ' B'; 84 | } 85 | } 86 | function addDefinitionAfter(after, definition, value, cclass) { 87 | dt = document.createElement('dt'); 88 | dt.appendChild(document.createTextNode(definition)); 89 | dd = document.createElement('dd'); 90 | if (cclass !== undefined) dd.className += cclass; 91 | dd.appendChild(document.createTextNode(value)); 92 | after.parentNode.insertBefore(dd, after.nextElementSibling.nextSibling); 93 | after.parentNode.insertBefore(dt, after.nextElementSibling.nextSibling); 94 | return dt; 95 | } 96 | function addDefinitionBefore(before, definition, value, cclass) { 97 | dt = document.createElement('dt'); 98 | dt.appendChild(document.createTextNode(definition)); 99 | dd = document.createElement('dd'); 100 | if (cclass !== undefined) dd.className += cclass; 101 | dd.appendChild(document.createTextNode(value)); 102 | before.parentNode.insertBefore(dt, before); 103 | before.parentNode.insertBefore(dd, before); 104 | return dt; 105 | } 106 | function addRawStats() { 107 | var tw, regExp = /([0-9,.]+)\s*([A-Z]+)/i; 108 | // Find text with raw stats 109 | tw = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^Raw Uploaded:/i.test(node.data); } }); 110 | if (tw.nextNode() == null) return; 111 | var rawUpMatch = tw.currentNode.nextElementSibling.textContent.match(regExp); 112 | tw = document.createTreeWalker(tw.currentNode.parentNode.parentNode, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^Raw Downloaded:/i.test(node.data); } }); 113 | if (tw.nextNode() == null) return; 114 | var rawDownMatch = tw.currentNode.nextElementSibling.textContent.match(regExp); 115 | tw = document.createTreeWalker(document.getElementById('content'), NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^\s*Ratio/i.test(node.data); } }); 116 | if (tw.nextNode() == null) return; 117 | var ratioNode = tw.currentNode.parentNode; 118 | tw = document.createTreeWalker(document.getElementById('content'), NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^\s*Uploaded/i.test(node.data); } }); 119 | if (tw.nextNode() == null) return; 120 | var ulNode = tw.currentNode.parentNode; 121 | tw = document.createTreeWalker(document.getElementById('content'), NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /^\s*Downloaded/i.test(node.data); } }); 122 | if (tw.nextNode() == null) return; 123 | var dlNode = tw.currentNode.parentNode; 124 | 125 | var ul = ulNode.nextElementSibling.textContent.match(regExp); 126 | var dl = dlNode.nextElementSibling.textContent.match(regExp); 127 | _debug && console.log(ul); 128 | _debug && console.log(dl); 129 | var uploaded = bytecount(parseFloat(ul[1].replace(/,/g, '')), ul[2].toUpperCase()); 130 | var downloaded = bytecount(parseFloat(dl[1].replace(/,/g, '')), dl[2].toUpperCase()); 131 | var rawuploaded = bytecount(parseFloat(rawUpMatch[1].replace(/,/g, '')), rawUpMatch[2].toUpperCase()); 132 | var rawdownloaded = bytecount(parseFloat(rawDownMatch[1].replace(/,/g, '')), rawDownMatch[2].toUpperCase()); 133 | var rawRatio = Infinity; 134 | if (bytecount(parseFloat(rawDownMatch[1].replace(/,/g, '')), rawDownMatch[2].toUpperCase()) > 0) 135 | rawRatio = (bytecount(parseFloat(rawUpMatch[1].replace(/,/g, '')), rawUpMatch[2].toUpperCase()) / bytecount(parseFloat(rawDownMatch[1].replace(/,/g, '')), rawDownMatch[2].toUpperCase())).toFixed(2); 136 | 137 | // Color ratio 138 | var color = 'r99'; 139 | if (rawRatio < 1) 140 | color = 'r' + ('0' + Math.ceil(10 * rawRatio)).slice(-2); 141 | else if (rawRatio < 5) 142 | color = 'r20'; 143 | else if (rawRatio < 99) 144 | color = 'r50'; 145 | 146 | // Add to user stats after ratio 147 | var hr = document.createElement('hr'); 148 | hr.style.clear = 'both'; 149 | ratioNode.parentNode.insertBefore(hr, ratioNode.nextElementSibling.nextSibling); 150 | var rawRatioNode = addDefinitionAfter(ratioNode, 'Raw Ratio:', rawRatio, color); 151 | addDefinitionAfter(ratioNode, 'Raw Downloaded:', rawDownMatch[0]); 152 | addDefinitionAfter(ratioNode, 'Raw Uploaded:', rawUpMatch[0]); 153 | ratioNode.nextElementSibling.title = 'Ratio\t Buffer'; 154 | rawRatioNode.nextElementSibling.title = 'Raw ratio\t Raw Buffer'; 155 | 156 | function printBuffer(u, d, r) { 157 | if (u / r - d >= 0) 158 | return '\n' + r.toFixed(1) + '\t' + (humancount(u / r - d)).slice(-10) + ' \tcan be downloaded' 159 | else 160 | return '\n' + r.toFixed(1) + '\t' + (humancount(d * r - u)).slice(-10) + ' \tmust be uploaded' 161 | } 162 | for (var i = 0; i < 10; i++) { 163 | var myRatio = [0.2, 0.5, 0.7, 0.8, 0.9, 1.0, 1.5, 2.0, 5.0, 10.0][i]; 164 | ratioNode.nextElementSibling.title += printBuffer(uploaded, downloaded, myRatio); 165 | rawRatioNode.nextElementSibling.title += printBuffer(rawuploaded, rawdownloaded, myRatio); 166 | } 167 | } 168 | function addYenPerStats() { 169 | var dpy = 365.256363; // days per year 170 | var tw = document.createTreeWalker(document.getElementById('content'), NodeFilter.SHOW_TEXT, { acceptNode: function (node) { return /Yen per day/i.test(node.data); } }); 171 | if (tw.nextNode() == null) return; 172 | var ypdNode = tw.currentNode.parentNode; 173 | var ypy = parseInt(ypdNode.nextElementSibling.textContent.replace(/[,¥]/g, ''), 10) * dpy; // Yen per year 174 | addDefinitionAfter(ypdNode, 'Yen per year:', formatInteger(Math.round(ypy * compoundInterest(1)))); 175 | addDefinitionAfter(ypdNode, 'Yen per month:', formatInteger(Math.round(ypy * compoundInterest(1 / 12)))); 176 | addDefinitionAfter(ypdNode, 'Yen per week:', formatInteger(Math.round(ypy * compoundInterest(7 / dpy)))); 177 | // 1 Yen = 1 MB = 1024^2 B * yen per year * interest for 1 s 178 | var hr = document.createElement('hr'); 179 | hr.style.clear = 'both'; 180 | ypdNode.parentNode.insertBefore(hr, ypdNode); 181 | addDefinitionBefore(ypdNode, 'Yen as upload:', humancount(Math.pow(1024, 2) * ypy * compoundInterest(1 / dpy / 24 / 60 / 60)) + '/s'); 182 | addDefinitionBefore(ypdNode, 'Yen per hour:', '¥' + (ypy * compoundInterest(1 / dpy / 24)).toFixed(1)); 183 | } 184 | if (delicious.settings.get('deliciousratio')) 185 | addRawStats(); 186 | if (delicious.settings.get('deliciousyenperx')) 187 | addYenPerStats(); 188 | })(); -------------------------------------------------------------------------------- /highlights-2/css/tfm_highlights.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name AB Highlights 2 Stylesheet 3 | @author TheFallingMan 4 | @namespace TheFallingMan 5 | @description Requires AB Highlights 2 scripe. 6 | @version 0.1.1 7 | ==/UserStyle== */ 8 | 9 | @-moz-document domain("animebytes.tv") { 10 | .torrent-page > .torrent-field { 11 | border-radius: 3px; 12 | padding-left: 1px; 13 | padding-right: 1px; 14 | padding-top: 0px; 15 | padding-bottom: 0px; 16 | background-color: #ececec; 17 | color: #000; 18 | border: 1px solid #d9d9d9; 19 | word-spacing: normal; 20 | } 21 | .torrent_table .group_torrent > td:first-child { 22 | word-spacing: -0.6ex; 23 | } 24 | 25 | .torrent-page > [data-audio-codec] { 26 | margin-right: -0.03ex; 27 | border-top-right-radius: 0; 28 | border-bottom-right-radius: 0; 29 | } 30 | 31 | .torrent-page > [data-group], .torrent-page > [data-region] { 32 | margin-left: 0; 33 | margin-right: 0; 34 | } 35 | 36 | .torrent-page > [data-source="Blu-ray"] { 37 | background-color: #a8ccf8; 38 | border-color:#a8ccf8; 39 | color: black; 40 | } 41 | .torrent-page > [data-source="Web"],.torrent-page > [data-source="TV"] { 42 | background-color: #ffc08f; 43 | border-color: #ffc08f; 44 | color: black; 45 | } 46 | .torrent-page > [data-source="UHD Blu-ray"] { 47 | background-color: #ecc2ec; 48 | border-color: #ecc2ec; 49 | color: black; 50 | } 51 | .torrent-page > [data-source="DVD"] { 52 | background-color: #fea5a5; 53 | border-color: #f8a8a8; 54 | color: black; 55 | } 56 | 57 | .torrent-page > [data-container] { 58 | background-color: #fea5a5; 59 | border-color: #f8a8a8; 60 | color: black; 61 | } 62 | .torrent-page > [data-container="MKV"] { 63 | background-color: #ecc2ec; 64 | border-color: #ecc2ec; 65 | color: black; 66 | } 67 | 68 | .torrent-page > [data-codec="h264 10-bit"] { 69 | background-color: #ffc08f; 70 | border-color: #ffc08f; 71 | color: black; 72 | } 73 | .torrent-page > [data-codec="h264"] { 74 | background-color: #fea5a5; 75 | border-color: #f8a8a8; 76 | color: black; 77 | } 78 | .torrent-page > [data-codec="h265"] { 79 | background-color: #a8ccf8; 80 | border-color:#a8ccf8; 81 | color: black; 82 | } 83 | .torrent-page > [data-codec="h265 10-bit"] { 84 | background-color: #ecc2ec; 85 | border-color: #ecc2ec; 86 | color: black; 87 | } 88 | 89 | .torrent-page > [data-resolution] { 90 | background-color: #fea5a5; 91 | border-color: #f8a8a8; 92 | color: black; 93 | } 94 | .torrent-page > [data-resolution="1080p"], .torrent-page > [data-resolution="4K"] { 95 | background-color: #ecc2ec; 96 | border-color: #ecc2ec; 97 | color: black; 98 | } 99 | .torrent-page > [data-resolution="720p"] { 100 | background-color: #a8ccf8; 101 | border-color:#a8ccf8; 102 | color: black; 103 | } 104 | .torrent-page > [data-resolution="480p"] { 105 | background-color: #abe4b0; 106 | border-color: #abe4b0; 107 | color: black; 108 | } 109 | 110 | .torrent-page > [data-audio-codec="FLAC"] { 111 | background-color: #ecc2ec; 112 | border-color: #ecc2ec; 113 | color: black; 114 | } 115 | .torrent-page > [data-audio-codec^="DTS"], .torrent-page > [data-audio-codec="AC3"], 116 | .torrent-page > [data-audio-codec="TrueHD"]{ 117 | background-color: #a8ccf8; 118 | border-color:#a8ccf8; 119 | color: black; 120 | } 121 | .torrent-page > [data-audio-codec="AAC"] { 122 | background-color: #abe4b0; 123 | border-color: #abe4b0; 124 | color: black; 125 | } 126 | 127 | .torrent-page > [data-audio-channels] { 128 | background-color: #abe4b0; 129 | border-color: #abe4b0; 130 | color: black; 131 | } 132 | .torrent-page > [data-audio-channels="5.1"] { 133 | background-color: #ecc2ec; 134 | border-color: #ecc2ec; 135 | color: black; 136 | } 137 | .torrent-page > [data-audio-channels="2.0"]{ 138 | background-color: #a8ccf8; 139 | border-color:#a8ccf8; 140 | color: black; 141 | } 142 | .torrent-page > [data-audio-channels] { 143 | border-top-left-radius: 0; 144 | border-bottom-left-radius: 0; 145 | border-left: 0.8px; 146 | border-left-style: solid; 147 | margin-left: -0.03ex; 148 | border-left-color: black; 149 | } 150 | 151 | .torrent-page > [data-dual-audio]{ 152 | background-color: #abe4b0; 153 | border-color: #abe4b0; 154 | color: black; 155 | } 156 | 157 | .torrent-page > [data-subbing="Softsubs"] { 158 | background-color: #ecc2ec; 159 | border-color: #ecc2ec; 160 | color: black; 161 | } 162 | .torrent-page > [data-subbing="Hardsubs"] { 163 | background-color: #ffc08f; 164 | border-color: #ffc08f; 165 | color: black; 166 | } 167 | .torrent-page > [data-subbing="RAW"]{ 168 | background-color: #fea5a5; 169 | border-color: #f8a8a8; 170 | color: black; 171 | } 172 | 173 | .torrent-page > [data-group]{ 174 | background-color: #6a6a6a; 175 | border-color: #6a6a6a; 176 | color: white; 177 | white-space: nowrap; 178 | } 179 | .torrent-page > [data-group="HorribleSubs"]{ 180 | background-color: #ffc08f; 181 | border-color: #ffc08f; 182 | color: black; 183 | } 184 | 185 | .torrent-page > [data-freeleech] > img { 186 | display:none; 187 | } 188 | .torrent-page > [data-freeleech]::after { 189 | content: "FL!"; 190 | } 191 | .torrent-page > [data-freeleech] { 192 | background-color: #fbefab; 193 | border-color: #fbefab; 194 | color: black; 195 | } 196 | .torrent-page > [data-snatched] { 197 | background-color: #1a2a90; 198 | border-color: #1a2a90; 199 | color: white; 200 | } 201 | .torrent-page[data-snatched] { 202 | opacity: 0.2; 203 | } 204 | .torrent-page[data-snatched]:hover { 205 | opacity: 1; 206 | } 207 | 208 | .torrent-page > [data-encoding="MP3"], .torrent-page > [data-source="CD"] { 209 | background-color: #abe4b0; 210 | border-color: #abe4b0; 211 | color: black; 212 | } 213 | .torrent-page > [data-encoding="FLAC"], .torrent-page > [data-bitrate="V0 (VBR)"], .torrent-page > [data-bitrate="320"], .torrent-page > [data-bitrate="Lossless"] { 214 | background-color: #a8ccf8; 215 | border-color:#a8ccf8; 216 | color: black; 217 | } 218 | .torrent-page > [data-bitrate="Lossless 24-bit"], .torrent-page > [data-log], .torrent-page > [data-cue] { 219 | background-color: #ecc2ec; 220 | border-color: #ecc2ec; 221 | color: black; 222 | } 223 | } -------------------------------------------------------------------------------- /highlights-2/dist/ab_highlights_2.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB Highlights 2 3 | // @namespace TheFallingMan 4 | // @version 2.3.3 5 | // @description Rewritten torrent highlighter. Adds attributes to torrent links, allowing CSS styling. 6 | // @author TheFallingMan 7 | // @icon https://animebytes.tv/favicon.ico 8 | // @match https://animebytes.tv/* 9 | // @license GPL-3.0 10 | // @require https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js 11 | // @require https://raw.githubusercontent.com/momentary0/AB-Userscripts/master/highlights-2/dist/bundle.js 12 | // ==/UserScript== 13 | 14 | require(["highlighter"], function(highlighter) { 15 | highlighter.main(); 16 | }); 17 | -------------------------------------------------------------------------------- /highlights-2/src/highlighter.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "./parser"; 2 | import { tokenise } from "./lexer"; 3 | import { SharedState, AnyState } from "./types"; 4 | 5 | export function highlight(links: NodeListOf, start: AnyState, className: string, defaultDelim?: string): number { 6 | const count = (needle: RegExp, haystack: string) => (haystack.match(needle) ?? []).length; 7 | 8 | const HIGHLIGHT_CLASS = 'userscript-highlight'; 9 | 10 | let success = 0; 11 | console.log(`Highlighting ${links.length} elements with ${className} class...`); 12 | const startTime = Date.now(); 13 | 14 | for (const el of links) { 15 | 16 | if (el.classList.contains(HIGHLIGHT_CLASS)) { 17 | console.error("Highlighter: Refusing to highlight element which is already " 18 | + 'highlighted', el); 19 | break; 20 | } 21 | 22 | let tokens = null; 23 | let output = null; 24 | let fields = null; 25 | 26 | try { 27 | el.classList.add(HIGHLIGHT_CLASS, className); 28 | 29 | let delim = defaultDelim; 30 | if (delim) { 31 | // use given delim. 32 | } else if (el.href.indexOf('torrents.php') != -1) { 33 | delim = ' | '; 34 | } else if (el.href.indexOf('torrents2.php') != -1) { 35 | delim = ' / '; 36 | } else { 37 | const pipes = count(/ \| /g, el.textContent!); 38 | const slashes = count(/ \/ /g, el.textContent!); 39 | delim = pipes > slashes ? ' | ' : ' / '; 40 | } 41 | 42 | tokens = tokenise(el.childNodes, delim); 43 | [output, fields] = parse(tokens, start); 44 | 45 | while (el.hasChildNodes()) el.removeChild(el.lastChild!); 46 | 47 | const df = document.createDocumentFragment(); 48 | df.append(...output); 49 | el.appendChild(df); 50 | 51 | let fieldsString = ''; 52 | for (const [k, v] of Object.entries(fields)) { 53 | el.dataset[k] = v; 54 | fieldsString += v.replace(/\s/g, '_') + ' '; 55 | } 56 | el.dataset.fields = fieldsString; 57 | 58 | if (fields.misc !== undefined) { 59 | throw 'misc'; 60 | } 61 | 62 | success++; 63 | } catch (e) { 64 | switch (e) { 65 | case 'misc': 66 | console.error('Highlighter: Generated data-misc field for torrent. ' 67 | + 'This might be due to a lexer/parser bug or unsupported data field.\n' 68 | + el.href + '\n' 69 | + JSON.stringify(el.textContent)); 70 | break; 71 | default: 72 | console.error("Highlighter: Fatal error while highlighting torrent: ", e); 73 | console.log("Element: ", el); 74 | // console.log("Child nodes: ", Array.from(el.childNodes)); 75 | console.log("Tokenised: ", tokens); 76 | // console.log("Converted: ", output); 77 | console.log("Data fields: ", fields); 78 | console.log("------------------------------------"); 79 | break; 80 | } 81 | } 82 | } 83 | 84 | console.log(`Done highlighting in ${Date.now()-startTime} ms: ${success} successful, ${links.length-success} failed.`); 85 | return success; 86 | } 87 | 88 | export function main() { 89 | const q = (s: string) => document.querySelectorAll(s) as NodeListOf; 90 | 91 | const TORRENT_PAGE_QUERY = '.group_torrent > td > a[href*="&torrentid="]'; 92 | highlight(q(TORRENT_PAGE_QUERY), SharedState.ARROW, 'torrent-page'); 93 | 94 | // torrents on search result pages are always separated by ' | '. 95 | const TORRENT_SEARCH_QUERY = '.torrent_properties > a[href*="&torrentid="]'; 96 | highlight(q(TORRENT_SEARCH_QUERY), SharedState.BEGIN_PARSE, 'torrent-page', ' | '); 97 | 98 | const TORRENT_BBCODE_QUERY = ':not(.group_torrent)>:not(.torrent_properties)>a[href*="/torrent/"]:not([title])'; 99 | highlight(q(TORRENT_BBCODE_QUERY), SharedState.BBCODE_LEFT, 'torrent-bbcode'); 100 | } 101 | 102 | export function test() { 103 | console.log("Testing..."); 104 | } 105 | -------------------------------------------------------------------------------- /highlights-2/src/lexer.ts: -------------------------------------------------------------------------------- 1 | import { Token, makeBasicToken, makeSeparatorToken, makeCompoundToken, makeElementToken, AnimeState, SNATCHED_TEXT, makeSpecialToken, ARROW, COLONS, DASH } from './types'; 2 | 3 | export function tokeniseString(input: string, delim: string): Token[] { 4 | switch (input) { 5 | case ARROW: 6 | return [makeSpecialToken('arrow', ARROW + ' ')]; 7 | case SNATCHED_TEXT: 8 | return [makeSpecialToken('snatched', 'Snatched')]; 9 | case DASH: 10 | return [makeSpecialToken('dash', DASH)]; 11 | case ' [': 12 | return [makeSpecialToken('lbracket', ' [')]; 13 | case ']': 14 | return [makeSpecialToken('rbracket', ']')]; 15 | } 16 | 17 | const RPAREN_WITH_SEP = ')' + delim; 18 | const LPAREN_OR_SEP = [ 19 | delim, 'Raw (', 'Translated (', 'Softsubs (', 'Hardsubs (', 'RAW (' 20 | ]; 21 | 22 | let i = 0; 23 | let j = 0; 24 | const output: Token[] = []; 25 | while (i < input.length) { 26 | if (j++ > 10000) { 27 | throw new Error("Iteration limit exceeded in tokeniseString"); 28 | } 29 | 30 | let remaining = input.slice(i); 31 | 32 | // find the next separator or compound expression, if it exists. 33 | let markerIndex = Infinity; 34 | let marker = null; 35 | for (const x of LPAREN_OR_SEP) { 36 | const n = remaining.indexOf(x); 37 | if (n >= 0 && n < markerIndex) { 38 | markerIndex = n; 39 | marker = x; 40 | } 41 | } 42 | 43 | // if no separator to the right, consume the rest of the string. 44 | if (marker === null) { 45 | output.push(makeBasicToken(remaining)); 46 | i += remaining.length; 47 | } else if (marker === delim) { 48 | // if next is a separator, consume up to that separator. 49 | if (markerIndex > 0) 50 | output.push(makeBasicToken(remaining.slice(0, markerIndex).trim())); 51 | output.push(makeSeparatorToken(delim)); 52 | i += markerIndex + marker.length; 53 | } else { 54 | // next is a compound expression. consume up to the close parens. 55 | i += marker.length; // consume the left and open paren. 56 | 57 | // find closing paren with separator. 58 | const inner = remaining.slice(marker.length); 59 | const closeSep = inner.indexOf(RPAREN_WITH_SEP); 60 | let right; 61 | if (closeSep < 0) { 62 | // no closing paren with separator, consume to end of string 63 | // and remove close paren character. 64 | right = inner.replace(/\)$/, ''); 65 | i += inner.length; 66 | } else { 67 | // consume to closing paren separator, excluding close paren. 68 | right = inner.substr(0, closeSep); 69 | if (right[right.length-1] == ')') 70 | right = right.substring(0, -1); 71 | i += closeSep + RPAREN_WITH_SEP.length; 72 | } 73 | 74 | output.push(makeCompoundToken(marker.split(' ')[0], right)); 75 | 76 | if (closeSep >= 0) { 77 | output.push(makeSeparatorToken(delim)); 78 | } 79 | } 80 | } 81 | return output; 82 | } 83 | 84 | export function tokeniseElement(input: HTMLElement): Token { 85 | return makeElementToken(input); 86 | } 87 | 88 | export function preTokenise(nodes: NodeListOf): (string | HTMLElement)[] { 89 | const output = []; 90 | 91 | let i = 0; 92 | for (const node of nodes) { 93 | switch (node.nodeType) { 94 | case Node.TEXT_NODE: 95 | let text = node.textContent!; 96 | 97 | if (i === 0) { 98 | if (text.startsWith(ARROW)) { 99 | output.push(ARROW); 100 | text = text.slice(1); 101 | } 102 | text = text.trimStart(); 103 | 104 | const colons = text.indexOf(COLONS); 105 | if (colons >= 0) { 106 | let left = text.slice(0, colons); 107 | 108 | let year = null; 109 | const yearMatch = / \[(\d{4})\]$/.exec(left); 110 | if (yearMatch) { 111 | year = yearMatch[1]; 112 | left = left.slice(0, yearMatch.index); 113 | } 114 | 115 | let right = null; 116 | const dash = left.lastIndexOf(DASH); 117 | if (dash != -1) { 118 | right = left.slice(dash + DASH.length); // everything after dash 119 | left = left.slice(0, dash); // everything before dash 120 | } 121 | output.push(left); 122 | 123 | if (right) { 124 | output.push(DASH); 125 | output.push(right); 126 | } 127 | if (year) { 128 | output.push(' ['); 129 | output.push(year); 130 | output.push(']'); 131 | } 132 | 133 | output.push(COLONS); 134 | text = text.slice(colons + COLONS.length).trimLeft(); 135 | } 136 | } 137 | if (i === nodes.length-1) { 138 | text = text.trimEnd(); 139 | if (text.endsWith(SNATCHED_TEXT.trimStart())) { 140 | output.push(text.replace(SNATCHED_TEXT.trimStart(), '').trimEnd()); 141 | text = SNATCHED_TEXT; 142 | } 143 | } 144 | 145 | output.push(text); 146 | break; 147 | case Node.ELEMENT_NODE: 148 | output.push(node as HTMLElement); 149 | break; 150 | default: 151 | throw new Error("Unknown child node: " + node); 152 | } 153 | i++; 154 | } 155 | return output; 156 | } 157 | 158 | export function mainTokenise(input: (string | HTMLElement)[], delim: string): Token[] { 159 | const output = []; 160 | for (const x of input) { 161 | if (typeof x == 'string') 162 | output.push(...tokeniseString(x, delim)); 163 | else 164 | output.push(tokeniseElement(x as HTMLElement)); 165 | } 166 | return output; 167 | } 168 | 169 | export function tokenise(nodes: NodeListOf, delim: string): Token[] { 170 | return mainTokenise(preTokenise(nodes), delim); 171 | } -------------------------------------------------------------------------------- /highlights-2/src/parser.ts: -------------------------------------------------------------------------------- 1 | import { Handler, AnimeState, Transformer, Token, BasicToken, SharedState, MusicState, BookState, GameState, makeCompoundToken, ARROW, AnyState, EPISODE_TEXT, SCENE_TEXT, SpecialTypes } from './types'; 2 | 3 | export type FinalOutput = (Node | string)[]; 4 | 5 | type PreSpan = { 6 | type: 'span', 7 | key: string, 8 | value: string, 9 | child: string | Node, 10 | }; 11 | type ElementOrString = PreSpan | string; 12 | type ParseOutput = ElementOrString[]; 13 | type TransformerResult = ElementOrString[] | ElementOrString | null; 14 | type TokenStateTransformer = Transformer; 15 | type SeenFields = {[key: string]: string}; 16 | 17 | export const span = (key: string, value: string, child?: HTMLElement | string): PreSpan => { 18 | return {type: 'span', key, value: value || key, child: child ?? value}; 19 | }; 20 | 21 | export type DFunction = (t: Token, s: SeenFields) => AnyState; 22 | export type TFunction = (t: Token, s: SeenFields) => TransformerResult 23 | 24 | export const preCapture = (pre: (t: Token, s: SeenFields) => [Token, SeenFields], 25 | transformer: TokenStateTransformer): TokenStateTransformer => { 26 | return (t, s) => { 27 | const [t1, s1] = pre(t, s); 28 | return transformer(t1, s1); 29 | } 30 | } 31 | 32 | function assertBasicToken(t: Token): asserts t is BasicToken { 33 | if (t.type != 'BASIC') 34 | throw new Error('Expected basic token, got ' + t); 35 | } 36 | 37 | export const basicTransformer = (key1: string, key2?: string): TFunction => { 38 | return (t) => { 39 | switch (t.type) { 40 | case 'BASIC': 41 | return span(key1, t.text); 42 | case 'COMPOUND': 43 | console.assert(!!key2); 44 | return [span(key1, t.left), ' (', span(key2!, t.right), ')']; 45 | default: 46 | throw new Error('Expected basic or compound token in basic transformer, got ' + t); 47 | } 48 | }; 49 | }; 50 | 51 | export const splitTransformer = (key1: string, key2: string): TFunction => { 52 | return (t) => { 53 | assertBasicToken(t); 54 | const i = t.text.lastIndexOf(' '); 55 | console.assert(i >= 0); 56 | return [span(key1, t.text.substr(0, i)), ' ', span(key2, t.text.slice(i+1))]; 57 | }; 58 | }; 59 | 60 | export const toCompoundToken = (t: Token): Token => { 61 | if (t.type !== 'BASIC') return t; 62 | 63 | const paren = t.text.indexOf(' ('); 64 | if (paren < 0 || t.text[t.text.length-1] !== ')') return t; 65 | 66 | const left = t.text.slice(0, paren); 67 | const right = t.text.slice(paren+2, t.text.length-1); 68 | return makeCompoundToken(left, right); 69 | }; 70 | 71 | export const captureDT = (decider: DFunction, transformer: TFunction): TokenStateTransformer => { 72 | return (t, s) => [decider(t, s), transformer(t, s)]; 73 | }; 74 | 75 | export const captureD = (decider: DFunction, key: string, key2?: string): TokenStateTransformer => 76 | captureDT(decider, basicTransformer(key, key2)); 77 | 78 | export const captureT = (next: AnyState, transform: TFunction): TokenStateTransformer => 79 | captureDT(() => next, transform); 80 | 81 | export const capture = (next: AnyState, key: string, key2?: string): TokenStateTransformer => 82 | captureD(() => next, key, key2); 83 | 84 | export const nullCapture = (next: AnyState): TokenStateTransformer => 85 | captureT(next, () => null); 86 | 87 | export type TokenStatePredicate = (t: Token, s: SeenFields) => boolean; 88 | export const switchCapture = (predicate: TokenStatePredicate, captureTrue: TokenStateTransformer, 89 | captureFalse: TokenStateTransformer): TokenStateTransformer => { 90 | return (t, s) => predicate(t, s) ? captureTrue(t, s) : captureFalse(t, s); 91 | }; 92 | 93 | export const isSpecial = (type: SpecialTypes): TokenStatePredicate => 94 | (t, s) => t.type === 'SPECIAL' && t.special === type; 95 | 96 | export const maybeFlag = (key: string, expected: string): TFunction => { 97 | return (t) => { 98 | if (t.type !== 'BASIC' || t.text !== expected) return null; 99 | return span(key, expected); 100 | } 101 | }; 102 | 103 | export const maybeList = (key: string, ...options: string[]): TFunction => { 104 | const set = new Set(options); 105 | return (t) => { 106 | if (t.type !== 'BASIC' || !set.has(t.text)) return null; 107 | return span(key, t.text); 108 | } 109 | }; 110 | 111 | export const basename = (url: string) => url.split('/').slice(-1)[0].split('.')[0]; 112 | 113 | export const maybeImage = (key: string, imageFile: string, value?: string): TFunction => { 114 | return (t) => { 115 | if (t.type !== 'ELEMENT' || t.element.tagName != 'IMG') return null; 116 | const srcFile = basename((t.element as HTMLImageElement).src); // safe cast due to tagName check 117 | if (srcFile === imageFile || srcFile.startsWith(imageFile + '-')) 118 | return span(key, value ?? key, t.element); 119 | else 120 | return null; 121 | }; 122 | } 123 | 124 | const TRAILING_IMAGES = [ 125 | maybeImage('freeleech', 'flicon', 'Freeleech'), 126 | maybeImage('hentai', 'hentai', 'Uncensored'), 127 | maybeImage('hentai', 'hentaic', 'Censored'), 128 | // Make highlight script compatible with Sneedex script https://animebytes.tv/forums.php?action=viewthread&threadid=28316 129 | maybeImage('sneedex', 'n40di4', 'Sneedex') 130 | ]; 131 | const fallbackTransformer = basicTransformer('misc', 'misc'); 132 | 133 | const trailingFieldsTransformer: TFunction = (t, s) => { 134 | if (t.type === 'SPECIAL' && t.special === 'snatched') { 135 | return [' - ', span('snatched', 'Snatched', 'Snatched')]; 136 | } 137 | 138 | if (t.type === 'BASIC') { 139 | if (t.text === SCENE_TEXT) 140 | return span('scene', SCENE_TEXT, SCENE_TEXT); 141 | if (t.text.startsWith(EPISODE_TEXT)) 142 | return span('episode', t.text.replace(EPISODE_TEXT, ''), t.text); 143 | } 144 | 145 | if (t.type !== 'ELEMENT') 146 | return fallbackTransformer(t, s); 147 | 148 | const imageMatches = TRAILING_IMAGES.map(trans => trans(t, s)).filter(x => x !== null); 149 | if (imageMatches) { 150 | return imageMatches[0]; 151 | } 152 | 153 | if (t.element.tagName === 'FONT' && t.element.textContent?.trim() == 'Exclusive!') { 154 | return span('exclusive', 'Exclusive', t.element); 155 | } 156 | 157 | return fallbackTransformer(t, s); 158 | }; 159 | 160 | const FIRST_FIELDS: {[s: string]: keyof typeof START_STATES} = { 161 | 'Blu-ray': 'anime', 'Web': 'anime', 'TV': 'anime', 162 | 'DVD': 'anime', 'UHD Blu-ray': 'anime', 'DVD5': 'anime', 163 | 'DVD9': 'anime', 'HD DVD': 'anime', 'VHS': 'anime', 164 | 'VCD': 'anime', 'LD': 'anime', 165 | 166 | 'MP3': 'music', 'FLAC': 'music', 'AAC': 'music', 167 | 168 | 'Game': 'game', 'Patch': 'game', 'DLC': 'game', 169 | 170 | 'Raw': 'book', 'Translated': 'book', 171 | }; 172 | 173 | const START_STATES = { 174 | anime: AnimeState.SOURCE, 175 | music: MusicState.ENCODING, 176 | game: GameState.TYPE, 177 | book: BookState.TRANSLATION, 178 | }; 179 | 180 | const initHandler: TokenStateTransformer = (t) => { 181 | if (t.type != 'BASIC' && t.type != 'COMPOUND') 182 | throw new Error('Need basic or compound as first token, not ' + t); 183 | 184 | const first = (t.type == 'COMPOUND' ? t.left : t.text); 185 | 186 | return [START_STATES[FIRST_FIELDS[first]] ?? SharedState.COMMON_TRAILING_FIELDS, null]; 187 | } 188 | 189 | const arrowTransformer: TFunction = (t) => { 190 | if (t.type == 'SPECIAL' && t.special == 'arrow') 191 | return t.text; 192 | return null; 193 | }; 194 | 195 | const specialTransformer: TFunction = (t) => { 196 | if (t.type !== 'SPECIAL') 197 | throw new Error('Expected special token.'); 198 | return t.text; 199 | } 200 | 201 | const GAME_REGIONS = ['Region Free', 'NTSC-J', 'NTSC-U', 'PAL', 'JPN', 'ENG', 'EUR']; 202 | const GAME_ARCHIVED = ['Archived', 'Unarchived']; 203 | const BOOK_FORMATS = ['Archived Scans', 'EPUB', 'PDF', 'Unarchived', 'Digital']; 204 | 205 | export const TRANSITION_ACTIONS: Handler = { 206 | // i'm very sorry for this code. 207 | [SharedState.BBCODE_LEFT]: capture(SharedState.BBCODE_DASH, 'left'), 208 | [SharedState.BBCODE_DASH]: switchCapture(isSpecial('dash'), 209 | captureT(SharedState.BBCODE_RIGHT, specialTransformer), nullCapture(SharedState.BBCODE_LBRACKET)), 210 | [SharedState.BBCODE_RIGHT]: capture(SharedState.BBCODE_LBRACKET, 'right'), 211 | [SharedState.BBCODE_LBRACKET]: switchCapture(isSpecial('lbracket'), 212 | captureT(SharedState.BBCODE_YEAR, specialTransformer), nullCapture(SharedState.COLONS)), 213 | [SharedState.BBCODE_YEAR]: capture(SharedState.BBCODE_RBRACKET, 'year'), 214 | [SharedState.BBCODE_RBRACKET]: captureT(SharedState.COLONS, specialTransformer), 215 | 216 | [SharedState.COLONS]: captureT(SharedState.BEGIN_PARSE, (t, s) => t.type === 'BASIC' ? t.text : null), 217 | [SharedState.ARROW]: captureT(SharedState.BEGIN_PARSE, arrowTransformer), 218 | [SharedState.BEGIN_PARSE]: initHandler, 219 | [SharedState.COMMON_TRAILING_FIELDS]: captureT(SharedState.COMMON_TRAILING_FIELDS, trailingFieldsTransformer), 220 | 221 | [AnimeState.SOURCE]: capture(AnimeState.CONTAINER, 'source'), 222 | [AnimeState.CONTAINER]: preCapture((t, s) => [toCompoundToken(t), s], 223 | captureD((t) => t.type === 'COMPOUND' ? AnimeState.ASPECT_RATIO : AnimeState.VIDEO_CODEC, 'container', 'region')), 224 | [AnimeState.ASPECT_RATIO]: captureD((t, s) => s.source == 'DVD9' || s.source == 'DVD5' 225 | ? AnimeState.RESOLUTION : AnimeState.VIDEO_CODEC, 'aspectRatio'), 226 | [AnimeState.VIDEO_CODEC]: capture(AnimeState.RESOLUTION, 'codec'), 227 | [AnimeState.RESOLUTION]: capture(AnimeState.AUDIO_CODEC, 'resolution'), 228 | [AnimeState.AUDIO_CODEC]: captureT(AnimeState.DUAL_AUDIO, splitTransformer('audioCodec', 'audioChannels')), 229 | [AnimeState.DUAL_AUDIO]: captureT(AnimeState.REMASTER, maybeFlag('dualAudio', 'Dual Audio')), 230 | [AnimeState.REMASTER]: captureT(AnimeState.SUBBING_AND_GROUP, maybeImage('remastered', 'rmstr', 'Remastered')), 231 | [AnimeState.SUBBING_AND_GROUP]: capture(SharedState.COMMON_TRAILING_FIELDS, 'subbing', 'group'), 232 | 233 | [MusicState.ENCODING]: capture(MusicState.BITRATE, 'encoding'), 234 | [MusicState.BITRATE]: capture(MusicState.SOURCE, 'bitrate'), 235 | [MusicState.SOURCE]: capture(MusicState.LOG, 'source'), 236 | [MusicState.LOG]: captureT(MusicState.CUE, maybeFlag('log', 'Log')), 237 | [MusicState.CUE]: captureT(SharedState.COMMON_TRAILING_FIELDS, maybeFlag('cue', 'Cue')), 238 | 239 | [GameState.TYPE]: capture(GameState.PLATFORM, 'type'), 240 | [GameState.PLATFORM]: capture(GameState.REGION, 'platform'), 241 | [GameState.REGION]: captureT(GameState.ARCHIVED, maybeList('region', ...GAME_REGIONS)), 242 | [GameState.ARCHIVED]: captureT(GameState.SCENE, maybeList('archived', ...GAME_ARCHIVED)), 243 | [GameState.SCENE]: captureT(SharedState.COMMON_TRAILING_FIELDS, maybeFlag('scene', 'Scene')), 244 | 245 | [BookState.TRANSLATION]: capture(BookState.FORMAT, 'translation', 'group'), 246 | [BookState.FORMAT]: captureT(BookState.ONGOING, maybeList('format', ...BOOK_FORMATS)), 247 | [BookState.ONGOING]: captureT(SharedState.COMMON_TRAILING_FIELDS, maybeFlag('ongoing', 'Ongoing')), 248 | }; 249 | 250 | export function preParse(tokens: Token[]): Token[] { 251 | return tokens; 252 | } 253 | 254 | export function mainParse(tokens: Token[], start: AnyState): [ParseOutput, SeenFields] { 255 | const output: ParseOutput = []; 256 | const seenFields: SeenFields = {}; 257 | 258 | let state: AnyState = start; 259 | let i = 0; 260 | let j = 0; 261 | while (i < tokens.length) { 262 | if (j++ > 10000) { 263 | throw new Error("Iteration limit exceeded in parse."); 264 | } 265 | const token = tokens[i]; 266 | if (token.type == 'SEPARATOR') { 267 | output.push(token.sep); 268 | i++; 269 | continue; 270 | } 271 | 272 | const handler: TokenStateTransformer = TRANSITION_ACTIONS[state]!; 273 | if (!handler) 274 | throw new Error("No handler associated with state: " + state); 275 | 276 | const [nextState, result] = handler(token, seenFields); 277 | // console.debug("Parse state transition from " + state + ": ", [nextState, result]); 278 | if (result != null) { 279 | i++; 280 | const resultArray = Array.isArray(result) ? result : [result]; 281 | for (const e of resultArray) { 282 | if (!(typeof e == 'string' ? e : e.child)) 283 | continue; 284 | if (typeof e != 'string' && e.key) 285 | seenFields[e.key] = e.value; 286 | output.push(e); 287 | } 288 | } 289 | state = nextState; 290 | } 291 | 292 | return [output, seenFields]; 293 | } 294 | 295 | const templateSpan = document.createElement('span'); 296 | templateSpan.className = 'userscript-highlight torrent-field'; 297 | 298 | export function postParse(parsed: ParseOutput): FinalOutput { 299 | return parsed.map(e => { 300 | if (typeof e == 'string') { 301 | return e; 302 | } else { 303 | const span = templateSpan.cloneNode(false) as HTMLSpanElement; 304 | if (e.key) 305 | span.dataset[e.key] = e.value; 306 | span.append(e.child); 307 | span.dataset.field = e.value; 308 | return span; 309 | } 310 | }); 311 | } 312 | 313 | export function parse(tokens: Token[], start: AnyState): [FinalOutput, SeenFields] { 314 | const [parsed, fields] = mainParse(preParse(tokens), start); 315 | return [postParse(parsed), fields]; 316 | } 317 | -------------------------------------------------------------------------------- /highlights-2/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type BaseToken = { 3 | type: string, 4 | }; 5 | 6 | export type BasicToken = { 7 | type: 'BASIC', 8 | text: string; 9 | }; 10 | 11 | export type CompoundToken = { 12 | type: 'COMPOUND', 13 | left: string, 14 | right: string, 15 | }; 16 | 17 | export type ElementToken = { 18 | type: 'ELEMENT', 19 | element: HTMLElement, 20 | }; 21 | 22 | export type SpecialTypes = 'snatched' | 'arrow' | 'dash' | 'lbracket' | 'rbracket'; 23 | export type SpecialToken = { 24 | type: 'SPECIAL', 25 | special: SpecialTypes, 26 | text: string, 27 | }; 28 | 29 | export type SeparatorToken = { 30 | type: 'SEPARATOR', 31 | sep: string, 32 | } 33 | 34 | export type Token = BasicToken | CompoundToken | SeparatorToken | ElementToken | SpecialToken; 35 | 36 | export const makeBasicToken = (text: string): BasicToken => ({ type: 'BASIC', text }); 37 | export const makeCompoundToken = (left: string, right: string): CompoundToken => ({ type: 'COMPOUND', left, right }); 38 | export const makeElementToken = (element: HTMLElement): ElementToken => ({ type: 'ELEMENT', element }); 39 | export const makeSpecialToken = (special: SpecialTypes, text: string): SpecialToken => ({ type: 'SPECIAL', special, text }); 40 | export const makeSeparatorToken = (sep: string, ): SeparatorToken => ({ type: 'SEPARATOR', sep }); 41 | 42 | 43 | export enum SharedState { 44 | ARROW, 45 | BBCODE_LEFT, 46 | BBCODE_DASH, 47 | BBCODE_RIGHT, 48 | BBCODE_LBRACKET, 49 | BBCODE_RBRACKET, 50 | BBCODE_YEAR, 51 | COLONS, 52 | BEGIN_PARSE, 53 | COMMON_TRAILING_FIELDS, // FL, exclusive, snatched, hentai, etc. 54 | } 55 | 56 | export enum AnimeState { 57 | SOURCE = 100, 58 | CONTAINER, 59 | ASPECT_RATIO, 60 | VIDEO_CODEC, 61 | RESOLUTION, 62 | AUDIO_CODEC, 63 | DUAL_AUDIO, 64 | SUBBING_AND_GROUP, 65 | REMASTER, 66 | TRAILING_FIELDS, 67 | } 68 | 69 | export enum MusicState { 70 | ENCODING = 200, 71 | BITRATE, 72 | SOURCE, 73 | LOG, 74 | CUE 75 | } 76 | 77 | export enum GameState { 78 | TYPE = 300, 79 | PLATFORM, 80 | REGION, 81 | ARCHIVED, 82 | SCENE 83 | } 84 | 85 | export enum BookState { 86 | TRANSLATION = 400, 87 | FORMAT, 88 | ONGOING 89 | } 90 | 91 | export type AnyState = SharedState | AnimeState | MusicState | BookState | GameState; 92 | 93 | export type Transformer = (token: T, state: H) => [S, U]; 94 | 95 | export type Handler = { 96 | // @ts-ignore 97 | [state in S]?: T 98 | }; 99 | 100 | export const SCENE_TEXT = 'Scene'; 101 | export const EPISODE_TEXT = 'Episode '; 102 | export const SNATCHED_TEXT = ' - Snatched'; 103 | export const ARROW = '»'; 104 | export const COLONS = ' :: '; 105 | export const DASH = ' - '; -------------------------------------------------------------------------------- /highlights-2/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | Torrent Highlighter Test 3 | 4 | 5 | 6 | 22 | 23 | -------------------------------------------------------------------------------- /highlights-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "amd", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | "outFile": "./dist/bundle.js", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | "removeComments": false, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | "sourceRoot": "https://raw.githubusercontent.com/momentary0/AB-Userscripts/master/highlights-2/src/", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | "mapRoot": "https://raw.githubusercontent.com/momentary0/AB-Userscripts/master/highlights-2/dist/", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "babel src -d lib" 4 | }, 5 | "devDependencies": { 6 | "babel-cli": "^6.26.0", 7 | "babel-core": "^6.26.3", 8 | "babel-loader": "^7.1.4", 9 | "babel-plugin-transform-regenerator": "^6.26.0", 10 | "babel-plugin-transform-runtime": "^6.23.0", 11 | "babel-polyfill": "^6.26.0", 12 | "babel-preset-env": "^1.6.1", 13 | "eslint": "^4.19.1", 14 | "webpack": "^4.6.0", 15 | "webpack-cli": "^2.0.15", 16 | "wrapper-webpack-plugin": "^1.0.0" 17 | }, 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /quick-links/src/ab_quick_links.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AB Quick Links 3 | // @namespace TheFallingMan 4 | // @version 0.2.0 5 | // @description Adds quick link dropdown to the main nav bar. 6 | // @author TheFallingMan 7 | // @icon https://animebytes.tv/favicon.ico 8 | // @match https://animebytes.tv/* 9 | // @license GPL-3.0 10 | // @grant GM_setValue 11 | // @grant GM_getValue 12 | // @require https://github.com/momentary0/AB-Userscripts/raw/master/delicious-library/src/ab_delicious_library.js 13 | // ==/UserScript== 14 | 15 | (function ABQuickLinks() { 16 | delicious.settings.init('ABQuickLinksEnabled', true); 17 | delicious.settings.init('ABQuickLinksLabel', 'Quick Links'); 18 | delicious.settings.init('ABQuickLinksPosition', 'navbar'); 19 | var defaultLinks = [ 20 | {text: 'Image Upload', href: '/imageupload.php'}, 21 | {text: 'Profile Settings', href: '/user.php?action=edit'}, 22 | ]; 23 | delicious.settings.init('ABQuickLinks', defaultLinks); 24 | 25 | if (delicious.settings.ensureSettingsInserted()) { 26 | var section = delicious.settings.createCollapsibleSection('Quick Links'); 27 | var s = section.querySelector('.settings_section_body'); 28 | s.appendChild(delicious.settings.createCheckbox( 29 | 'ABQuickLinksEnabled', 30 | 'Enable/Disable', 31 | 'Adds dropdown quick links to the main nav bar.' 32 | )); 33 | s.appendChild(delicious.settings.createTextSetting( 34 | 'ABQuickLinksLabel', 35 | 'Label', 36 | 'Label to display on dropdown.' 37 | )); 38 | s.appendChild(delicious.settings.createDropDown( 39 | 'ABQuickLinksPosition', 40 | 'Position', 41 | 'Where quick links will be displayed.', 42 | [['Nav bar', 'navbar'], ['User info', 'userinfo']] 43 | )); 44 | s.appendChild(delicious.settings.createRowSetting( 45 | 'ABQuickLinks', 46 | 'Links', 47 | [['Label', 'text', 'text'], ['Link', 'href', 'text']], 48 | null, 49 | {newButtonText: '+ Add Link'} 50 | )); 51 | delicious.settings.insertSection(section); 52 | } 53 | 54 | function generateSubnavUL() { 55 | var subnavUL = document.createElement('ul'); 56 | subnavUL.className = 'subnav nobullet'; 57 | subnavUL.style.display = 'none'; 58 | 59 | subnavUL.style.width = '100%'; 60 | // subnavUL.style.width = 'fit-content'; 61 | 62 | var links = delicious.settings.get('ABQuickLinks'); 63 | for (var i = 0; i < links.length; i++) { 64 | var li = document.createElement('li'); 65 | li.style.width = '100%'; 66 | var a = document.createElement('a'); 67 | a.style.width = '100%'; 68 | a.style.boxSizing = 'border-box'; 69 | // a.style.cssText += '; padding-right: 20px !important;'; 70 | 71 | a.style.textOverflow = 'ellipsis'; 72 | a.style.overflow = 'hidden'; 73 | a.style.whiteSpace = 'nowrap'; 74 | 75 | if (links[i]['href']) 76 | a.href = links[i]['href']; 77 | a.textContent = links[i]['text'] || ''; 78 | li.appendChild(a); 79 | subnavUL.appendChild(li); 80 | } 81 | 82 | return subnavUL; 83 | } 84 | 85 | var rootLI = document.createElement('li'); 86 | rootLI.id = 'nav_quicklinks'; 87 | rootLI.className = 'navmenu'; 88 | // Above search boxes, but below user menu dropdown. 89 | rootLI.style.zIndex = 94; 90 | 91 | var label = delicious.settings.get('ABQuickLinksLabel'); 92 | rootLI.innerHTML = '\ 93 | '; 94 | rootLI.firstElementChild.addEventListener('click', delicious.utilities.toggleSubnav); 95 | rootLI.firstElementChild.insertAdjacentText('afterbegin', label); 96 | 97 | var subnav = generateSubnavUL(); 98 | rootLI.appendChild(subnav); 99 | 100 | if (delicious.settings.get('ABQuickLinksPosition') === 'navbar') { 101 | document.querySelector('.main-menu').appendChild(rootLI); 102 | subnav.style.left = '-1px'; 103 | } else { 104 | document.querySelector('#yen_count').insertAdjacentElement('beforebegin', rootLI); 105 | } 106 | })(); -------------------------------------------------------------------------------- /torrent-highlighter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /torrent-highlighter/README.md: -------------------------------------------------------------------------------- 1 | ## AnimeBytes Torrent Highlighter -------------------------------------------------------------------------------- /torrent-highlighter/css/tfm_torrent_styles.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name AnimeBytes Torrent Page Stylesheet 3 | @author TheFallingMan 4 | @namespace TheFallingMan 5 | @description Depends on torrent highlighter script. 6 | @version 0.1.1 7 | ==/UserStyle== */ 8 | 9 | @-moz-document domain("animebytes.tv") { 10 | .torrent-page > .torrent-field { 11 | border-radius: 3px; 12 | padding-left: 1px; 13 | padding-right: 1px; 14 | padding-top: 0px; 15 | padding-bottom: 0px; 16 | background-color: #ececec; 17 | color: #000; 18 | border: 1px solid #d9d9d9; 19 | word-spacing: normal; 20 | } 21 | .torrent_table .group_torrent > td:first-child { 22 | word-spacing: -0.6ex; 23 | } 24 | 25 | .torrent-page > [data-audio-codec] { 26 | margin-right: -0.03ex; 27 | border-top-right-radius: 0; 28 | border-bottom-right-radius: 0; 29 | } 30 | 31 | .torrent-page > [data-group], .torrent-page > [data-region] { 32 | margin-left: 0; 33 | margin-right: 0; 34 | } 35 | 36 | .torrent-page > [data-source="Blu-ray"] { 37 | background-color: #a8ccf8; 38 | border-color:#a8ccf8; 39 | color: black; 40 | } 41 | .torrent-page > [data-source="Web"],.torrent-page > [data-source="TV"] { 42 | background-color: #ffc08f; 43 | border-color: #ffc08f; 44 | color: black; 45 | } 46 | .torrent-page > [data-source="UHD Blu-ray"] { 47 | background-color: #ecc2ec; 48 | border-color: #ecc2ec; 49 | color: black; 50 | } 51 | .torrent-page > [data-source="DVD"] { 52 | background-color: #fea5a5; 53 | border-color: #f8a8a8; 54 | color: black; 55 | } 56 | 57 | .torrent-page > [data-container] { 58 | background-color: #fea5a5; 59 | border-color: #f8a8a8; 60 | color: black; 61 | } 62 | .torrent-page > [data-container="MKV"] { 63 | background-color: #ecc2ec; 64 | border-color: #ecc2ec; 65 | color: black; 66 | } 67 | 68 | .torrent-page > [data-codec="h264 10-bit"] { 69 | background-color: #ffc08f; 70 | border-color: #ffc08f; 71 | color: black; 72 | } 73 | .torrent-page > [data-codec="h264"] { 74 | background-color: #fea5a5; 75 | border-color: #f8a8a8; 76 | color: black; 77 | } 78 | .torrent-page > [data-codec="h265"] { 79 | background-color: #a8ccf8; 80 | border-color:#a8ccf8; 81 | color: black; 82 | } 83 | .torrent-page > [data-codec="h265 10-bit"] { 84 | background-color: #ecc2ec; 85 | border-color: #ecc2ec; 86 | color: black; 87 | } 88 | 89 | .torrent-page > [data-resolution] { 90 | background-color: #fea5a5; 91 | border-color: #f8a8a8; 92 | color: black; 93 | } 94 | .torrent-page > [data-resolution="1080p"], .torrent-page > [data-resolution="4K"] { 95 | background-color: #ecc2ec; 96 | border-color: #ecc2ec; 97 | color: black; 98 | } 99 | .torrent-page > [data-resolution="720p"] { 100 | background-color: #a8ccf8; 101 | border-color:#a8ccf8; 102 | color: black; 103 | } 104 | .torrent-page > [data-resolution="480p"] { 105 | background-color: #abe4b0; 106 | border-color: #abe4b0; 107 | color: black; 108 | } 109 | 110 | .torrent-page > [data-audio-codec="FLAC"] { 111 | background-color: #ecc2ec; 112 | border-color: #ecc2ec; 113 | color: black; 114 | } 115 | .torrent-page > [data-audio-codec^="DTS"], .torrent-page > [data-audio-codec="AC3"], 116 | .torrent-page > [data-audio-codec="TrueHD"]{ 117 | background-color: #a8ccf8; 118 | border-color:#a8ccf8; 119 | color: black; 120 | } 121 | .torrent-page > [data-audio-codec="AAC"] { 122 | background-color: #abe4b0; 123 | border-color: #abe4b0; 124 | color: black; 125 | } 126 | 127 | .torrent-page > [data-audio-channels] { 128 | background-color: #abe4b0; 129 | border-color: #abe4b0; 130 | color: black; 131 | } 132 | .torrent-page > [data-audio-channels="5.1"] { 133 | background-color: #ecc2ec; 134 | border-color: #ecc2ec; 135 | color: black; 136 | } 137 | .torrent-page > [data-audio-channels="2.0"]{ 138 | background-color: #a8ccf8; 139 | border-color:#a8ccf8; 140 | color: black; 141 | } 142 | .torrent-page > [data-audio-channels] { 143 | border-top-left-radius: 0; 144 | border-bottom-left-radius: 0; 145 | border-left: 0.8px; 146 | border-left-style: solid; 147 | margin-left: -0.03ex; 148 | border-left-color: black; 149 | } 150 | 151 | .torrent-page > [data-dual-audio]{ 152 | background-color: #abe4b0; 153 | border-color: #abe4b0; 154 | color: black; 155 | } 156 | 157 | .torrent-page > [data-subbing="Softsubs"] { 158 | background-color: #ecc2ec; 159 | border-color: #ecc2ec; 160 | color: black; 161 | } 162 | .torrent-page > [data-subbing="Hardsubs"] { 163 | background-color: #ffc08f; 164 | border-color: #ffc08f; 165 | color: black; 166 | } 167 | .torrent-page > [data-subbing="RAW"]{ 168 | background-color: #fea5a5; 169 | border-color: #f8a8a8; 170 | color: black; 171 | } 172 | 173 | .torrent-page > [data-group]{ 174 | background-color: #6a6a6a; 175 | border-color: #6a6a6a; 176 | color: white; 177 | white-space: nowrap; 178 | } 179 | .torrent-page > [data-group="HorribleSubs"]{ 180 | background-color: #ffc08f; 181 | border-color: #ffc08f; 182 | color: black; 183 | } 184 | 185 | .torrent-page > [data-freeleech] > img { 186 | display:none; 187 | } 188 | .torrent-page > [data-freeleech]::after { 189 | content: "FL!"; 190 | } 191 | .torrent-page > [data-freeleech] { 192 | background-color: #fbefab; 193 | border-color: #fbefab; 194 | color: black; 195 | } 196 | .torrent-page > [data-snatched] { 197 | background-color: #1a2a90; 198 | border-color: #1a2a90; 199 | color: white; 200 | } 201 | .torrent-page[data-snatched] { 202 | opacity: 0.2; 203 | } 204 | .torrent-page[data-snatched]:hover { 205 | opacity: 1; 206 | } 207 | 208 | .torrent-page > [data-encoding="MP3"], .torrent-page > [data-source="CD"] { 209 | background-color: #abe4b0; 210 | border-color: #abe4b0; 211 | color: black; 212 | } 213 | .torrent-page > [data-encoding="FLAC"], .torrent-page > [data-bitrate="V0 (VBR)"], .torrent-page > [data-bitrate="320"], .torrent-page > [data-bitrate="Lossless"] { 214 | background-color: #a8ccf8; 215 | border-color:#a8ccf8; 216 | color: black; 217 | } 218 | .torrent-page > [data-bitrate="Lossless 24-bit"], .torrent-page > [data-log], .torrent-page > [data-cue] { 219 | background-color: #ecc2ec; 220 | border-color: #ecc2ec; 221 | color: black; 222 | } 223 | } -------------------------------------------------------------------------------- /torrent-highlighter/css/torrent_styles.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name TFM's Torrent Page Stylesheet (eva's highlighter version) 3 | @author TheFallingMan 4 | @namespace TheFallingMan 5 | @description Compatible with eva's torrent highlighter. 6 | @version 0.1.1 7 | ==/UserStyle== */ 8 | 9 | @-moz-document domain("animebytes.tv") { 10 | .group_torrent .userscript-highlight > span { 11 | border-radius: 3px; 12 | padding-left: 1px; 13 | padding-right: 1px; 14 | padding-top: 0px; 15 | padding-bottom: 0px; 16 | background-color: #ececec; 17 | color: #000; 18 | border: 1px solid #d9d9d9; 19 | word-spacing: normal; 20 | } 21 | .group_torrent > td:first-child { 22 | word-spacing: -0.6ex; 23 | } 24 | 25 | .group_torrent .userscript-highlight > span[data-audio-codec] { 26 | margin-left: -0.03ex; 27 | border-top-right-radius: 0; 28 | border-bottom-right-radius: 0; 29 | } 30 | 31 | .group_torrent .userscript-highlight > span[data-group], .group_torrent .userscript-highlight > span[data-region] { 32 | margin-left: 0; 33 | margin-right: 0; 34 | } 35 | 36 | .group_torrent .userscript-highlight > span[data-source="Blu-ray"] { 37 | background-color: #a8ccf8; 38 | border-color:#a8ccf8; 39 | color: black; 40 | } 41 | .group_torrent .userscript-highlight > span[data-source="Web"],.group_torrent .userscript-highlight > span[data-source="TV"] { 42 | background-color: #ffc08f; 43 | border-color: #ffc08f; 44 | color: black; 45 | } 46 | .group_torrent .userscript-highlight > span[data-source="UHD Blu-ray"] { 47 | background-color: #ecc2ec; 48 | border-color: #ecc2ec; 49 | color: black; 50 | } 51 | .group_torrent .userscript-highlight > span[data-source="DVD"] { 52 | background-color: #fea5a5; 53 | border-color: #f8a8a8; 54 | color: black; 55 | } 56 | 57 | .group_torrent .userscript-highlight > span[data-container] { 58 | background-color: #fea5a5; 59 | border-color: #f8a8a8; 60 | color: black; 61 | } 62 | .group_torrent .userscript-highlight > span[data-container="MKV"] { 63 | background-color: #ecc2ec; 64 | border-color: #ecc2ec; 65 | color: black; 66 | } 67 | 68 | .group_torrent .userscript-highlight > span[data-codec="h264 10-bit"] { 69 | background-color: #ffc08f; 70 | border-color: #ffc08f; 71 | color: black; 72 | } 73 | .group_torrent .userscript-highlight > span[data-codec="h264"] { 74 | background-color: #fea5a5; 75 | border-color: #f8a8a8; 76 | color: black; 77 | } 78 | .group_torrent .userscript-highlight > span[data-codec="h265"] { 79 | background-color: #a8ccf8; 80 | border-color:#a8ccf8; 81 | color: black; 82 | } 83 | .group_torrent .userscript-highlight > span[data-codec="h265 10-bit"] { 84 | background-color: #ecc2ec; 85 | border-color: #ecc2ec; 86 | color: black; 87 | } 88 | 89 | .group_torrent .userscript-highlight > span[data-resolution] { 90 | background-color: #fea5a5; 91 | border-color: #f8a8a8; 92 | color: black; 93 | } 94 | .group_torrent .userscript-highlight > span[data-resolution="1080p"], .group_torrent .userscript-highlight > span[data-resolution="4K"] { 95 | background-color: #ecc2ec; 96 | border-color: #ecc2ec; 97 | color: black; 98 | } 99 | .group_torrent .userscript-highlight > span[data-resolution="720p"] { 100 | background-color: #a8ccf8; 101 | border-color:#a8ccf8; 102 | color: black; 103 | } 104 | .group_torrent .userscript-highlight > span[data-resolution="480p"] { 105 | background-color: #abe4b0; 106 | border-color: #abe4b0; 107 | color: black; 108 | } 109 | 110 | .group_torrent .userscript-highlight > span[data-audio-codec="FLAC"] { 111 | background-color: #ecc2ec; 112 | border-color: #ecc2ec; 113 | color: black; 114 | } 115 | .group_torrent .userscript-highlight > span[data-audio-codec^="DTS"], .group_torrent .userscript-highlight > span[data-audio-codec="AC3"], 116 | .group_torrent .userscript-highlight > span[data-audio-codec="TrueHD"]{ 117 | background-color: #a8ccf8; 118 | border-color:#a8ccf8; 119 | color: black; 120 | } 121 | .group_torrent .userscript-highlight > span[data-audio-codec="AAC"] { 122 | background-color: #abe4b0; 123 | border-color: #abe4b0; 124 | color: black; 125 | } 126 | 127 | .group_torrent .userscript-highlight > span[data-audio-channels] { 128 | background-color: #abe4b0; 129 | border-color: #abe4b0; 130 | color: black; 131 | } 132 | .group_torrent .userscript-highlight > span[data-audio-channels="5.1"] { 133 | background-color: #ecc2ec; 134 | border-color: #ecc2ec; 135 | color: black; 136 | } 137 | .group_torrent .userscript-highlight > span[data-audio-channels="2.0"]{ 138 | background-color: #a8ccf8; 139 | border-color:#a8ccf8; 140 | color: black; 141 | } 142 | .group_torrent .userscript-highlight > span[data-audio-channels] { 143 | border-top-left-radius: 0; 144 | border-bottom-left-radius: 0; 145 | border-left: 0.8px; 146 | border-left-style: solid; 147 | margin-left: -0.03ex; 148 | border-left-color: black; 149 | } 150 | 151 | .group_torrent .userscript-highlight > span[data-dual-audio]{ 152 | background-color: #abe4b0; 153 | border-color: #abe4b0; 154 | color: black; 155 | } 156 | 157 | .group_torrent .userscript-highlight > span[data-subbing="Softsubs"] { 158 | background-color: #ecc2ec; 159 | border-color: #ecc2ec; 160 | color: black; 161 | } 162 | .group_torrent .userscript-highlight > span[data-subbing="Hardsubs"] { 163 | background-color: #ffc08f; 164 | border-color: #ffc08f; 165 | color: black; 166 | } 167 | .group_torrent .userscript-highlight > span[data-subbing="RAW"]{ 168 | background-color: #fea5a5; 169 | border-color: #f8a8a8; 170 | color: black; 171 | } 172 | 173 | .group_torrent .userscript-highlight > span[data-group]{ 174 | background-color: #6a6a6a; 175 | border-color: #6a6a6a; 176 | color: white; 177 | } 178 | .group_torrent .userscript-highlight > span[data-group="HorribleSubs"]{ 179 | background-color: #ffc08f; 180 | border-color: #ffc08f; 181 | color: black; 182 | } 183 | 184 | .group_torrent .userscript-highlight > span[data-freeleech] > img { 185 | display:none; 186 | } 187 | .group_torrent .userscript-highlight > span[data-freeleech]::after { 188 | content: "FL!"; 189 | } 190 | .group_torrent .userscript-highlight > span[data-freeleech] { 191 | background-color: #fbefab; 192 | border-color: #fbefab; 193 | color: black; 194 | } 195 | .group_torrent .userscript-highlight[data-snatched] { 196 | opacity: 0.35; 197 | } 198 | .group_torrent .userscript-highlight[data-snatched]:hover { 199 | opacity: 1; 200 | } 201 | .group_torrent .userscript-highlight > [data-encoding="MP3"], .group_torrent .userscript-highlight > [data-media="CD"] { 202 | background-color: #abe4b0; 203 | border-color: #abe4b0; 204 | color: black; 205 | } 206 | .group_torrent .userscript-highlight > [data-encoding="FLAC"], .group_torrent .userscript-highlight > [data-bitrate="V0 (VBR)"], .group_torrent .userscript-highlight > [data-bitrate="320"], .group_torrent .userscript-highlight > [data-bitrate="Lossless"] { 207 | background-color: #a8ccf8; 208 | border-color:#a8ccf8; 209 | color: black; 210 | } 211 | .group_torrent .userscript-highlight > [data-bitrate="Lossless 24-bit"], .group_torrent .userscript-highlight > [data-log], .group_torrent .userscript-highlight > [data-cue] { 212 | background-color: #ecc2ec; 213 | border-color: #ecc2ec; 214 | color: black; 215 | } 216 | } -------------------------------------------------------------------------------- /torrent-highlighter/dist/tfm_torrent_highlighter.min.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TFM's torrent highlighter 3 | // @namespace TheFallingMan 4 | // @version 0.1.1 5 | // @description Adds attributes to torrent links, allowing CSS styling. 6 | // @author TheFallingMan 7 | // @icon https://animebytes.tv/favicon.ico 8 | // @match https://animebytes.tv/* 9 | // @license GPL-3.0 10 | // ==/UserScript== 11 | 12 | !function(e){var t={};function __webpack_require__(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:false,exports:{}};return e[n].call(r.exports,r,r.exports,__webpack_require__),r.l=true,r.exports}__webpack_require__.m=e,__webpack_require__.c=t,__webpack_require__.d=function(e,t,n){__webpack_require__.o(e,t)||Object.defineProperty(e,t,{configurable:false,enumerable:true,get:n})},__webpack_require__.r=function(e){Object.defineProperty(e,"__esModule",{value:true})},__webpack_require__.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(t,"a",t),t},__webpack_require__.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=0)}([function(e,t,n){"use strict";var r=function(){function defineProperties(e,t){for(var n=0;n=this.fields.length&&e!==d.FINISHED&&(e=O[d.INSERT_DOCFRAG].call(this))}}},{key:"getNext",value:function(){return this.fields[this.index++]}},{key:"peekNext",value:function(){return this.fields[this.index]}},{key:"newSpan",value:function(e,t,n){this.linkElement.dataset[t]=n;var r=this.spanTemplate.cloneNode(false);return r.dataset[t]=n,e.nodeType?(this.nodeAppended=true,r.appendChild(e)):r.textContent=e,r}},{key:"appendSpan",value:function(e,t,n){this.docFrag.appendChild(this.newSpan(e,t,n))}},{key:"appendText",value:function(e){this.docFrag.appendChild(document.createTextNode(e))}},{key:"appendDelim",value:function(){this.docFrag.appendChild(document.createTextNode(this.delim))}},{key:"clearChildren",value:function(){for(;this.linkElement.hasChildNodes();)this.linkElement.removeChild(this.linkElement.lastChild)}}]),TorrentPropertyParser}();function newCaptureHandler(e,t){var n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];return function(){return n&&this.appendDelim(),this.appendSpan(this.peekNext(),e,this.peekNext()),this.index++,t}}function newFlagHandler(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return function(){var i=this.peekNext();if(e===i)this.index++,this.appendDelim(),this.appendSpan(i,t,"");else if(null!==r)return r;return n}}function newListHandler(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return function(){var i=this.peekNext();if(-1!==e.indexOf(i))this.index++,this.appendDelim(),this.appendSpan(i,t,i);else if(null!==r)return r;return n}}for(var d=Object.freeze({INITIALISE:-11,DETECTING:-1,BEGIN:-2,COMMON_TRAILING_FIELDS:-3,INSERT_DOCFRAG:-5,FINISHED:false,ERROR:-10}),l=Object.freeze({ENCODING:1,BITRATE:2,SOURCE:3,LOG:4,CUE:5}),p=(_defineProperty(e={},d.BEGIN,newCaptureHandler("encoding",l.BITRATE,false)),_defineProperty(e,l.BITRATE,newCaptureHandler("bitrate",l.SOURCE)),_defineProperty(e,l.SOURCE,newCaptureHandler("source",l.LOG)),_defineProperty(e,l.LOG,newFlagHandler("Log","log",l.CUE)),_defineProperty(e,l.CUE,newFlagHandler("Cue","cue",d.COMMON_TRAILING_FIELDS)),e),o=Object.freeze({SOURCE:1,CONTAINER:2,ASPECT_RATIO:3,VIDEO_CODEC:4,RESOLUTION:5,AUDIO_CODEC_AND_CHANNELS:6,DUAL_AUDIO:7,SUBBING_AND_GROUP:8,REMASTER:9,TRAILING_FIELDS:10}),h=(_defineProperty(t={},d.BEGIN,newCaptureHandler("source",o.CONTAINER,false)),_defineProperty(t,o.CONTAINER,function(){var e=this.getNext();if(")"===e.charAt(e.length-1)){var t=e.substr(0,e.indexOf(" (")),n=e.slice(e.indexOf(" (")+2,-1);return this.appendDelim(),this.appendSpan(t,"container",t),this.appendText(" ("),this.appendSpan(n,"region",n),this.appendText(")"),o.ASPECT_RATIO}return this.appendDelim(),this.appendSpan(e,"container",e),o.VIDEO_CODEC}),_defineProperty(t,o.ASPECT_RATIO,function(){var e=this.getNext();return this.appendDelim(),this.appendSpan(e,"aspectRatio",e),"DVD5"===this.fields[0]||"DVD9"===this.fields[0]?o.RESOLUTION:o.VIDEO_CODEC}),_defineProperty(t,o.VIDEO_CODEC,newCaptureHandler("codec",o.RESOLUTION)),_defineProperty(t,o.RESOLUTION,newCaptureHandler("resolution",o.AUDIO_CODEC_AND_CHANNELS)),_defineProperty(t,o.AUDIO_CODEC_AND_CHANNELS,function(){var e=this.getNext(),t=e.substr(0,e.lastIndexOf(" ")),n=e.substr(e.lastIndexOf(" ")+1);return"."!==n.charAt(1)?d.ERROR:(this.appendDelim(),this.appendSpan(t,"audioCodec",t),this.appendText(" "),this.appendSpan(n,"audioChannels",n),o.DUAL_AUDIO)}),_defineProperty(t,o.DUAL_AUDIO,newFlagHandler("Dual Audio","dualAudio",o.REMASTER)),_defineProperty(t,o.REMASTER,function(){var e=this.peekNext();return"IMG"===e.tagName&&"Remastered"===e.alt&&(this.index++,this.appendDelim(),this.appendSpan(e,"remastered","")),o.SUBBING_AND_GROUP}),_defineProperty(t,o.SUBBING_AND_GROUP,function(){if(u.call(this))return d.COMMON_TRAILING_FIELDS;var e=this.getNext();if(!e.nodeType){var t=(e=e.trim()).substr(0,e.indexOf(" "));if("Softsubs"===t||"Hardsubs"===t||"RAW"===t){this.appendDelim(),this.appendSpan(t,"subbing",t),this.appendText(" (");for(var n=e.substr(e.indexOf(" (")+2);!n.endsWith(")")&&this.indextd>a[href*="&torrentid="]'),I=new s,C=0;Ca[href*="&torrentid="]'),S=0;S:not(.torrent_properties)>a[href*="&torrentid="]:not([title])'),m=0;m=0,i=o&&n.regeneratorRuntime;if(n.regeneratorRuntime=void 0,t.exports=r(3),o)n.regeneratorRuntime=i;else try{delete n.regeneratorRuntime}catch(t){n.regeneratorRuntime=void 0}},function(t,e){!function(e){"use strict";var r,n=Object.prototype,o=n.hasOwnProperty,i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",u=i.asyncIterator||"@@asyncIterator",c=i.toStringTag||"@@toStringTag",s="object"==typeof t,l=e.regeneratorRuntime;if(l)s&&(t.exports=l);else{(l=e.regeneratorRuntime=s?t.exports:{}).wrap=wrap;var h="suspendedStart",f="suspendedYield",p="executing",d="completed",y={},_={};_[a]=function(){return this};var v=Object.getPrototypeOf,g=v&&v(v(values([])));g&&g!==n&&o.call(g,a)&&(_=g);var m=GeneratorFunctionPrototype.prototype=Generator.prototype=Object.create(_);GeneratorFunction.prototype=m.constructor=GeneratorFunctionPrototype,GeneratorFunctionPrototype.constructor=GeneratorFunction,GeneratorFunctionPrototype[c]=GeneratorFunction.displayName="GeneratorFunction",l.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===GeneratorFunction||"GeneratorFunction"===(e.displayName||e.name))},l.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,GeneratorFunctionPrototype):(t.__proto__=GeneratorFunctionPrototype,c in t||(t[c]="GeneratorFunction")),t.prototype=Object.create(m),t},l.awrap=function(t){return{__await:t}},defineIteratorMethods(AsyncIterator.prototype),AsyncIterator.prototype[u]=function(){return this},l.AsyncIterator=AsyncIterator,l.async=function(t,e,r,n){var o=new AsyncIterator(wrap(t,e,r,n));return l.isGeneratorFunction(e)?o:o.next().then(function(t){return t.done?t.value:o.next()})},defineIteratorMethods(m),m[c]="Generator",m[a]=function(){return this},m.toString=function(){return"[object Generator]"},l.keys=function(t){var e=[];for(var r in t)e.push(r);return e.reverse(),function next(){for(;e.length;){var r=e.pop();if(r in t)return next.value=r,next.done=false,next}return next.done=true,next}},l.values=values,Context.prototype={constructor:Context,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=r,this.done=false,this.delegate=null,this.method="next",this.arg=r,this.tryEntries.forEach(resetTryEntry),!t)for(var e in this)"t"===e.charAt(0)&&o.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=r)},stop:function(){this.done=true;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var e=this;function handle(n,o){return a.type="throw",a.arg=t,e.next=n,o&&(e.method="next",e.arg=r),!!o}for(var n=this.tryEntries.length-1;n>=0;--n){var i=this.tryEntries[n],a=i.completion;if("root"===i.tryLoc)return handle("end");if(i.tryLoc<=this.prev){var u=o.call(i,"catchLoc"),c=o.call(i,"finallyLoc");if(u&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&o.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),resetTryEntry(r),y}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;resetTryEntry(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:values(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=r),y}}}function wrap(t,e,r,n){var o=Object.create((e&&e.prototype instanceof Generator?e:Generator).prototype),i=new Context(n||[]);return o._invoke=function(t,e,r){var n=h;return function(o,i){if(n===p)throw Error("Generator is already running");if(n===d){if("throw"===o)throw i;return doneResult()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var u=maybeInvokeDelegate(a,r);if(u){if(u===y)continue;return u}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===h)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=p;var c=tryCatch(t,e,r);if("normal"===c.type){if(n=r.done?d:f,c.arg===y)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n=d,r.method="throw",r.arg=c.arg)}}}(t,r,i),o}function tryCatch(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}function Generator(){}function GeneratorFunction(){}function GeneratorFunctionPrototype(){}function defineIteratorMethods(t){["next","throw","return"].forEach(function(e){t[e]=function(t){return this._invoke(e,t)}})}function AsyncIterator(t){var e;this._invoke=function(r,n){function callInvokeWithMethodAndArg(){return new Promise(function(e,i){!function invoke(e,r,n,i){var a=tryCatch(t[e],t,r);if("throw"!==a.type){var u=a.arg,c=u.value;return c&&"object"==typeof c&&o.call(c,"__await")?Promise.resolve(c.__await).then(function(t){invoke("next",t,n,i)},function(t){invoke("throw",t,n,i)}):Promise.resolve(c).then(function(t){u.value=t,n(u)},i)}i(a.arg)}(r,n,e,i)})}return e=e?e.then(callInvokeWithMethodAndArg,callInvokeWithMethodAndArg):callInvokeWithMethodAndArg()}}function maybeInvokeDelegate(t,e){var n=t.iterator[e.method];if(n===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=r,maybeInvokeDelegate(t,e),"throw"===e.method))return y;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return y}var o=tryCatch(n,t.iterator,e.arg);if("throw"===o.type)return e.method="throw",e.arg=o.arg,e.delegate=null,y;var i=o.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=r),e.delegate=null,y):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,y)}function pushTryEntry(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function resetTryEntry(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function Context(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(pushTryEntry,this),this.reset(true)}function values(t){if(t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,i=function next(){for(;++n} 20 | */ 21 | const field_mapping = { 22 | "Blu-ray":10, 23 | "Web":34, 24 | "TV":35, 25 | "DVD": 30, 26 | "UHD Blu-ray":9, 27 | 'DVD5': 31, 28 | 'DVD9':32, 29 | "HD DVD":29, 30 | "VHS":50, 31 | "VCD":50, 32 | "LD": 50, 33 | 34 | 'MKV':5, 35 | 'M2TS':50, 36 | 'ISO':40, 37 | 'AVI':20, 38 | 'VOB IFO':42, 39 | 'MP4':22, 40 | 'OGM':25, 41 | 'WMV':26, 42 | 'MPG':23, 43 | 'MPEG':22, 44 | 'VOB':43, 45 | 'TS':49, 46 | 47 | "16:9":5, 48 | "4:3":6, 49 | "1.85:1":4, 50 | "2.39:1":3, 51 | "2.4:1":2, 52 | 53 | "h264": 21, 54 | "h264 10-bit": 20, 55 | "h265": 11, 56 | "h265 10-bit": 10, 57 | "XviD": 25, 58 | "DivX": 25, 59 | //"WMV": 26, 60 | "MPEG-TS": 23, 61 | //"MPEG": 22, 62 | "VC-1": 24, 63 | 64 | '480p': 30, 65 | '720p':20, 66 | '1080p':15, 67 | '1080i':16, 68 | '4K':10, 69 | '2560x1440': 13, 70 | 71 | "FLAC":10, 72 | "AAC":20, 73 | "AC3":19, 74 | "MP3":30, 75 | "Vorbis":21, 76 | "Opus":21, 77 | "TrueHD":11, 78 | "DTS":17, 79 | "DTS-ES":14, 80 | "PCM":5, 81 | "WMA":22, 82 | "Real Audio":23, 83 | "DTS-HD MA":12, 84 | "DTS-HD":13, 85 | 86 | '7.1': 1, 87 | '6.1': 5, 88 | '6.0': 6, 89 | '5.1': 10, 90 | '5.0':11, 91 | '2.1': 20, 92 | '2.0': 21, 93 | '1.0': 30, 94 | 95 | 'Dual Audio': 5, 96 | 'Softsubs': 10, 97 | 'Hardsubs':12, 98 | 'RAW': 15, 99 | }; 100 | 101 | /** 102 | * Returns a generator iterating over the properties of the given 103 | * , from left to right. 104 | * 105 | * @example 106 | * var iter = row_to_field_list($('tr')); 107 | * iter.next().value; // Blu-ray 108 | * iter.next().value; // MKV 109 | * iter.next().value; // h265 110 | * 111 | * @param {HTMLTableRowElement} torrent_row Row element to parse 112 | */ 113 | function *row_to_field_list(torrent_row) { 114 | /* Structure is something like: 115 | 116 | 117 | [DL | RP] 118 | Blu-ray | MKV | h265 | ... 119 | 120 | 121 | */ 122 | let link = torrent_row.firstElementChild.children[1]; 123 | //console.log(torrent_row); 124 | ///console.log(link); 125 | 126 | // (Experimental) compatibility with eva's torrent highlighter. 127 | // We check if the link contains spans. 128 | if (link.firstElementChild && link.firstElementChild.tagName === 'SPAN') { 129 | // In this case, our work is done and we just need to return 130 | // each span's text. 131 | isDebug() && console.log('span'); 132 | let spans = link.children; 133 | for (let i = 0; i < spans.length; i++) { 134 | yield (spans[i].textContent); 135 | } 136 | } else { 137 | // Otherwise, we split and return the fields ourselves. 138 | isDebug() && console.log('textContent'); 139 | let split_fields = link.textContent.replace('»', '').trim().split(' | '); 140 | for (let i = 0; i < split_fields.length; i++) { 141 | let field = split_fields[i]; 142 | // This handles sub groups and region codes. 143 | // Will work incorrectly if a sub group contains | 144 | if (field.indexOf('(') !== -1 && field.charAt(field.length-1) === ')') { 145 | let s = field.split(' ('); 146 | yield (s[0]); 147 | yield (s[1].trim(')')); 148 | } else if (field.charAt(field.length-2) === '.') { 149 | // If the second last char is a . we assume it's an 150 | // audio channel field. 151 | let s = field.split(' '); 152 | yield (s.slice(0, -1).join(' ')); 153 | yield (s[s.length-1]); 154 | } else { 155 | yield (field); 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Gets the sorting weight of the value `x` from the defined field mapping. 163 | * @param {string} x Field value 164 | */ 165 | function get_weight(x) { 166 | return field_mapping[x]; 167 | } 168 | 169 | /** 170 | * Iterates through each property of a and b from left to right, 171 | * comparing each property in turn using get_weight(). 172 | * 173 | * For use as a comparison function in the .sort() method. Returns a positive 174 | * number when b < a, negative for a < b, and 0 if a = b. 175 | * @param {HTMLTableRowElement} a 176 | * @param {HTMLTableRowElement} b 177 | */ 178 | function sort_comparer(a, b) { 179 | let a_iter = row_to_field_list(a); 180 | let b_iter = row_to_field_list(b); 181 | 182 | let a_object = {}; 183 | while (!a_object.done) { 184 | a_object = a_iter.next(); 185 | var b_object = b_iter.next(); 186 | if (b_object.done) { 187 | // In case of one string being shorter than the other, 188 | // we sort the shorter one first. 189 | return 1; 190 | } 191 | let a_value = a_object.value; 192 | let b_value = b_object.value; 193 | if (a_value === b_value) { 194 | continue; 195 | } 196 | let a_weight = get_weight(a_value); 197 | let b_weight = get_weight(b_value); 198 | //_debug && (`a: ${a_value} ${a_weight}; b: ${b_value} ${b_weight}`) 199 | // Doing an arithmetic comparison only makes sense when both 200 | // a and b have defined weights. 201 | if (a_weight && b_weight) { 202 | // This integer subtraction results in the correct sorting. 203 | return a_weight - b_weight; 204 | } else { 205 | // Use string (alphabetical) sort on the original strings. 206 | return (a_value < b_value) ? -1 : 1; 207 | } 208 | } 209 | // If a and b have the same number of elements, they will finish at 210 | // the same time and are equal. 211 | if (b_object.done) 212 | return 0; 213 | // Otherwise, a < b. 214 | return -1; 215 | } 216 | 217 | /** 218 | * Sorts the torrent_rows, considering info rows. 219 | * Skips torrent groups when 3 or less torrents. 220 | * 221 | * @param {Arraya { 23 | background: none; 24 | font-family: 'Century Gothic', sans-serif; 25 | font-weight: bold; 26 | color: #f6f6f7; 27 | font-size: 1.4em; 28 | letter-spacing: -0.2pt; 29 | } 30 | 31 | #logo>a:hover { 32 | background: none; 33 | color: #989ba0; 34 | } 35 | `; 36 | insertCSS(css); 37 | 38 | var logo = document.querySelector('#logo a'); 39 | logo.textContent = 'animebytes'; 40 | 41 | window.addEventListener('load', function() { 42 | var iconLink = document.querySelector('link[rel="shortcut icon"]'); 43 | iconLink = document.createElement('link'); 44 | iconLink.rel = 'icon'; 45 | iconLink.type = 'image/x-icon'; 46 | iconLink.href = favicon; 47 | document.head.appendChild(iconLink); 48 | }); 49 | })(); -------------------------------------------------------------------------------- /webpack_base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | module.exports = function(scriptName, scriptExt, scriptRoot, minify=false) { 7 | const headerDoc = fs.readFileSync(scriptRoot + '/src/' + scriptName+scriptExt, 'utf8'); 8 | let userscriptHeader = ''; 9 | for (const line of headerDoc.split('\n')) { 10 | console.log(line); 11 | if (!line.startsWith('//')) 12 | break; 13 | userscriptHeader += line + '\n'; 14 | } 15 | 16 | return { 17 | entry: scriptRoot + '/src/' + scriptName+scriptExt, 18 | output: { 19 | filename: scriptName + (minify?'.min':'') + scriptExt, 20 | path: path.resolve(scriptRoot, 'dist') 21 | }, 22 | module: { 23 | rules: [ 24 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 25 | ] 26 | }, 27 | mode: 'none', 28 | devtool: 'source-map', 29 | plugins: (minify?[ 30 | new UglifyJsPlugin({ 31 | cache: true, 32 | uglifyOptions: { 33 | warnings: true, 34 | compress: { 35 | 'booleans': false, 36 | 'unused': true, 37 | 'dead_code': true, 38 | 'conditionals': true, 39 | 'passes': 2, 40 | 'evaluate': true, 41 | 'inline': true, 42 | 'pure_funcs': ['Date.now'], 43 | 'pure_getters': true, 44 | 'unsafe': true, 45 | global_defs: { 46 | 47 | } 48 | }, 49 | mangle: { 50 | keep_fnames: true, 51 | } 52 | } 53 | })]:[ 54 | /*new UglifyJsPlugin({ 55 | uglifyOptions: { 56 | compress: { 57 | 'unused': true, 58 | 'dead_code': true, 59 | 'conditionals': true, 60 | 'passes': 2, 61 | 'evaluate': true, 62 | 'inline': true, 63 | 'pure_funcs': ['console.log', 'Date.now', '_log'], 64 | 'unsafe': true 65 | }, 66 | mangle: false, 67 | output: { 68 | comments: 'all', 69 | beautify: true, 70 | } 71 | } 72 | })]*/ 73 | ]) .concat([ 74 | new webpack.BannerPlugin({ 75 | raw: true, 76 | banner: userscriptHeader, 77 | entryOnly: true 78 | })]) 79 | }; 80 | }; --------------------------------------------------------------------------------