├── LICENSE
├── README.md
└── subclean.user.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Amir Hossein
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Subclean - Subscene Subtitle List Cleaner
2 |
3 | Subclean is a userscript designed to enhance the user experience on Subscene by cleaning up and organizing the subtitle list on movie pages. The script aims to address issues such as duplicate subtitles and poorly formatted titles. By merging duplicates and categorizing the text, Subclean provides a more readable and streamlined subtitle list.
4 |
5 | 
6 |
7 |
8 | ## Features
9 |
10 | ### 1. Remove Unnecessary Text
11 | - **Name:** Remove unnecessary text
12 | - **Description:** Eliminate redundant information in the title
13 |
14 | ### 2. Info Cleanup
15 | - **Name:** Info Cleanup
16 | - **Description:** Categorize and organize subtitle information, including seasons, episodes, codecs, resolutions, and qualities.
17 |
18 | ## Installation
19 |
20 | 1. Install a userscript manager extension for your browser:
21 |
22 | * [Tampermonkey](https://www.tampermonkey.net/)
23 | * [Violentmonkey](https://violentmonkey.github.io/get-it/)
24 | * [Greasemonkey](https://addons.mozilla.org/firefox/addon/greasemonkey/)
25 |
26 | 2. Click [here](https://github.com/SamadiPour/Subclean/raw/main/subclean.user.js) to install the Subclean userscript.
27 |
28 | ## Usage
29 |
30 | 1. Visit a movie page on Subscene, such as `https://subscene.com/subtitles/*`.
31 | 2. The script will automatically clean up the subtitle list by removing duplicates and improving text formatting.
32 | 3. Optionally, use the provided features to customize the display by enabling or disabling them via the userscript manager menu.
33 |
34 | ## Configuration
35 |
36 | Subclean provides customization options through the userscript manager menu. You can toggle features on or off based on your preferences.
37 |
38 | 
39 |
40 | ## Contributions
41 |
42 | Feel free to contribute to the project by submitting issues or pull requests on the [GitHub repository](https://github.com/SamadiPour/Subclean).
43 |
44 | ## License
45 |
46 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/SamadiPour/Subclean/blob/main/LICENSE) file for details.
47 |
--------------------------------------------------------------------------------
/subclean.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Subclean
3 | // @namespace http://github.com/SamadiPour
4 | // @author http://github.com/SamadiPour
5 | // @version 1.3
6 | // @description Subscene subtitle list cleaner
7 | // @match https://subscene.com/subtitles/*
8 | // @icon https://subscene.com/favicon.ico
9 | // @grant GM_registerMenuCommand
10 | // ==/UserScript==
11 |
12 | // Features
13 | const features = {
14 | cleanText: {
15 | name: 'Remove unnecessary text',
16 | default: true,
17 | key: 'subclean_clean_text_feature'
18 | },
19 | textClassification: {
20 | name: 'Info Cleanup',
21 | default: true,
22 | key: 'subclean_parse_text_feature'
23 | }
24 | };
25 |
26 | // Parameters
27 | const cleanMovieNameStringValues = ["'", ":", "?", "."];
28 |
29 | (function () {
30 | 'use strict';
31 |
32 | // Get features and register menu items
33 | initializeFeatures();
34 |
35 | // Main function
36 | function removeDuplicates() {
37 | const uniqueElements = {};
38 | const rows = document.querySelector('table').querySelectorAll('tr');
39 |
40 | // group duplicates
41 | rows.forEach((row) => {
42 | const anchorElement = row.querySelector('td.a1 a');
43 | if (anchorElement) {
44 | const href = anchorElement.getAttribute('href');
45 | if (href && !uniqueElements[href]) {
46 | uniqueElements[href] = row;
47 | } else {
48 | mergeDuplicateRows(uniqueElements[href], anchorElement);
49 | row.parentNode.removeChild(row);
50 | }
51 | }
52 | });
53 |
54 | // clean the text
55 | if (features.cleanText.enabled) {
56 | modifySubtitleText()
57 | }
58 | }
59 |
60 | function observeDOM() {
61 | var targetNode = document.body;
62 | var config = { childList: true, subtree: true };
63 | var callback = function (mutationsList, observer) {
64 | if (document.querySelector('table')) {
65 | observer.disconnect();
66 | removeDuplicates();
67 | }
68 | };
69 | var observer = new MutationObserver(callback);
70 | observer.observe(targetNode, config);
71 | }
72 |
73 | // ========== Additional functions ==========
74 |
75 | function initializeFeatures() {
76 | for (const feature in features) {
77 | const { name, key, default: defaultValue } = features[feature];
78 | const isEnabled = JSON.parse(localStorage.getItem(key)) ?? defaultValue;
79 |
80 | features[feature].enabled = isEnabled;
81 |
82 | GM_registerMenuCommand(
83 | isEnabled ? `${name} - Enabled` : `${name} - Disabled`,
84 | () => toggleFeature(key, isEnabled)
85 | );
86 | }
87 | }
88 |
89 | function toggleFeature(featureKey, currentValue) {
90 | localStorage.setItem(featureKey, !currentValue);
91 |
92 | if (featureKey === features.textClassification.key && !currentValue) {
93 | localStorage.setItem(features.cleanText.key, true);
94 | } else if (featureKey === features.cleanText.key && !currentValue) {
95 | localStorage.setItem(features.textClassification.key, false);
96 | }
97 |
98 | location.reload();
99 | }
100 |
101 | function capitalizeFirstLetter(string) {
102 | return string.charAt(0).toUpperCase() + string.slice(1);
103 | }
104 |
105 | function mergeDuplicateRows(originalRow, anchorElement) {
106 | const origElement = originalRow.querySelector('td.a1 a');
107 | const spanElement = document.createElement('span');
108 | spanElement.className = 'l r';
109 |
110 | if (!features.textClassification.enabled) {
111 | spanElement.textContent = '\u200C';
112 | }
113 |
114 | origElement.appendChild(spanElement);
115 | origElement.appendChild(anchorElement.children[1]);
116 | }
117 |
118 |
119 | function modifySubtitleText() {
120 | // css
121 | var style = document.createElement('style');
122 | style.innerHTML = '.subtitles td.a1 span {white-space: pre-line;} .subtitles td.a1 span.l {white-space: initial}';
123 | document.head.appendChild(style);
124 |
125 | // logic
126 | const movieName = getMovieNameFromPage();
127 | const movieNameClean = cleanString(movieName.lastIndexOf(' - ') === -1 ? movieName.trim() : movieName.substring(0, movieName.lastIndexOf(' - ')).trim());
128 | const newRows = document.getElementsByTagName('table')[0].querySelectorAll('tr');
129 | newRows.forEach((row) => {
130 | const anchorElement = row.querySelector('td.a1 a');
131 | if (anchorElement) {
132 | const spans = anchorElement.querySelectorAll('span:nth-child(even)');
133 | let info = [];
134 | if (spans) {
135 | spans.forEach((span) => {
136 | var title = span.innerText;
137 | title = title.replace(/\./g, ' ');
138 | title = title.replace(RegExp(escapeRegExp(movieNameClean), 'gi'), '').trim();
139 | title = title.replace(/(19|20)\d{2}/gm, '');
140 | title = title.replace(/\(\s*\)/g, '').trim();
141 | title = title.replace(/ {2,}/g, ' ').trim();
142 | title = title.replace(/[\[\]]/g, '').trim();
143 | if (features.textClassification.enabled) {
144 | info.push(title);
145 | anchorElement.removeChild(span);
146 | } else {
147 | span.innerText = title ? title : movieName;
148 | }
149 | });
150 |
151 | if (features.textClassification.enabled) {
152 | const cleanInfo = cleanUpInfo(info);
153 | const spanElement = document.createElement('span');
154 | spanElement.innerHTML = cleanInfo;
155 | anchorElement.appendChild(spanElement);
156 | }
157 | }
158 | }
159 | });
160 | }
161 |
162 | function escapeRegExp(str) {
163 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
164 | }
165 |
166 | function cleanString(str) {
167 | let result = str;
168 | for (let value of cleanMovieNameStringValues) {
169 | result = result.replace(RegExp(escapeRegExp(value), 'gi'), '');
170 | }
171 | return result;
172 | }
173 |
174 | // Most regexes in the following functions are from https://gitlab.com/tinyMediaManager/tinyMediaManager.
175 | function getSeason(string) {
176 | let r1 = /(staffel|season|saison|series|temporada)[\s_.-]?(\d{1,4})/i;
177 |
178 | let m1 = string.match(r1);
179 | if (m1) {
180 | return m1[0];
181 | }
182 |
183 | return [];
184 | }
185 |
186 | function getSeasonAndEpisode(string) {
187 | const r1 = /s(\d{1,4})[ ]?((?:([epx.-]+\d{1,4})+))/gi
188 | const r2 = /(\d{1,4})(?=x)((?:([epx]+\d{1,4})+))/gi
189 |
190 | const m1 = string.match(r1);
191 | const m2 = string.match(r2);
192 |
193 | if (m1) {
194 | return m1.concat(m2 || []);
195 | }
196 |
197 | return m2 || [];
198 | }
199 |
200 | function getCodecs(string) {
201 | const r1 = /((?:[hx]\.?\s?264)|(?:[hx]\.?265)|(?:hevc))/gi
202 | // also get 10bit
203 | const r2 = /((?:10bit))/gi
204 |
205 | const m1 = string.match(r1);
206 | const m2 = string.match(r2);
207 |
208 | if (m1) {
209 | return m1.concat(m2 || []);
210 | }
211 |
212 | return m2 || [];
213 | }
214 |
215 | function getResolution(string) {
216 | const r1 = /((?:\d{3,4}[p|i]))/gi
217 | // also 2k, 4k, 8k
218 | const r2 = /((?:\d{1}[k]))/gi
219 |
220 | const m1 = string.match(r1);
221 | const m2 = string.match(r2);
222 |
223 | if (m1) {
224 | return m1.concat(m2 || []);
225 | }
226 |
227 | return m2 || [];
228 | }
229 |
230 | function getQuality(string) {
231 | const qualities = [
232 | '(uhd|ultrahd)[ .\-]?(bluray|blueray|bdrip|brrip|dbrip|bd25|bd50|bdmv|blu\-ray)',
233 | '(bluray|blueray|bdrip|brrip|dbrip|bd25|bd50|bdmv|blu\-ray)', '(dvd|video_ts|dvdrip|dvdr)', '(hddvd|hddvdrip)', '(tv|hdtv|pdtv|dsr|dtb|dtt|dttv|dtv|hdtvrip|tvrip|dvbrip)',
234 | '(vhs|vhsrip)', '(laserdisc|ldrip)', 'D-VHS', '(hdrip)', '(cam)', '(\sts|telesync|hdts|ht\-ts)', '(tc|telecine|hdtc|ht\-tc)', '(dvdscr)',
235 | '(\sr5)', '(webrip)', '(web-dl|webdl|web)'
236 | ];
237 |
238 | // return all matches
239 | const r1 = new RegExp(qualities.join('|'), 'gi');
240 |
241 | const m1 = string.match(r1);
242 |
243 | if (m1) {
244 | return m1;
245 | }
246 |
247 | return [];
248 | }
249 |
250 | function getMovieNameFromPage() {
251 | return document.getElementsByClassName('header')[0].querySelector('h2').textContent.trim().split('\n')[0];
252 | }
253 |
254 |
255 | function cleanUpInfo(strings) {
256 | // get all info and store them in info object
257 | const info = {
258 | seasons: [],
259 | episodes: [],
260 | codecs: [],
261 | resolutions: [],
262 | qualities: []
263 | };
264 |
265 | strings.forEach(string => {
266 | info.seasons.push(getSeason(string));
267 | info.episodes.push(getSeasonAndEpisode(string));
268 | info.codecs.push(getCodecs(string));
269 | info.resolutions.push(getResolution(string));
270 | info.qualities.push(getQuality(string));
271 | });
272 |
273 | // flatten arrays
274 | info.seasons = info.seasons.flat();
275 | info.episodes = info.episodes.flat();
276 | info.codecs = info.codecs.flat();
277 | info.resolutions = info.resolutions.flat();
278 | info.qualities = info.qualities.flat();
279 |
280 | // remove duplicates
281 | info.seasons = [...new Set(info.seasons)];
282 | info.episodes = [...new Set(info.episodes)];
283 | info.codecs = [...new Set(info.codecs)];
284 | info.resolutions = [...new Set(info.resolutions)];
285 | info.qualities = [...new Set(info.qualities)];
286 |
287 | // sort based on length
288 | info.seasons.sort((a, b) => b.length - a.length);
289 | info.episodes.sort((a, b) => b.length - a.length);
290 | info.codecs.sort((a, b) => b.length - a.length);
291 | info.resolutions.sort((a, b) => b.length - a.length);
292 | info.qualities.sort((a, b) => b.length - a.length);
293 |
294 | // remove all info from strings
295 | for (let key in info) {
296 | info[key].forEach(item => {
297 | strings.forEach((string, index) => {
298 | strings[index] = string.replace(item, '');
299 | });
300 | });
301 | }
302 |
303 | // remove all special characters and whitespaces
304 | let additional = []
305 | strings.forEach((string, index) => {
306 | let str = strings[index];
307 | // remove special characters
308 | str = str.replace(/[^a-zA-Z0-9\s]/g, '');
309 | str = str.replace(/\s+/g, ' ');
310 | str = str.replace(/\s\-/g, ' ');
311 | str = str.replace(/\-\s/g, ' ');
312 | str = str.trim();
313 |
314 | additional.push(str);
315 | });
316 |
317 | // remove duplicates
318 | additional = [...new Set(additional)];
319 | additional = additional.filter(str => str.trim() !== '');
320 |
321 | // clean codecs
322 | var cleanCodecs = info.codecs;
323 | cleanCodecs = cleanCodecs.map(codec => codec.toUpperCase().replace(/\s/g, ''));
324 | for (var i = 0; i < cleanCodecs.length; i++) {
325 | if (cleanCodecs[i].startsWith("H") && cleanCodecs.includes("X" + cleanCodecs[i].substring(1))) {
326 | cleanCodecs.splice(i, 1);
327 | i--; // Adjust index after removal
328 | }
329 | }
330 | info.codecs = cleanCodecs;
331 |
332 | let result = "";
333 | // first write info with key
334 | if (info.seasons.length > 0) {
335 | result += "Seasons: " + info.seasons.join(', ') + "
";
336 | }
337 | if (info.episodes.length > 0) {
338 | result += "Episode: " + info.episodes.join(', ') + "
";
339 | }
340 | if (info.resolutions.length > 0) {
341 | result += "Resolutions: " + info.resolutions.join(', ') + "
";
342 | }
343 | if (info.codecs.length > 0) {
344 | result += "Codecs: " + info.codecs.join(', ') + "
";
345 | }
346 | if (info.qualities.length > 0) {
347 | result += "Releases: " + info.qualities.join(', ') + "
";
348 | }
349 | if (additional.length > 0) {
350 | result += "Encoders: " + additional.join(', ');
351 | }
352 |
353 | // return restult or if it's empty, just the name of the movie!
354 | return result.trim() === "" ? getMovieNameFromPage() : result;
355 | }
356 |
357 | observeDOM();
358 | })();
359 |
--------------------------------------------------------------------------------