-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathzzzzzzzz.user.js
More file actions
6097 lines (5474 loc) · 293 KB
/
zzzzzzzz.user.js
File metadata and controls
6097 lines (5474 loc) · 293 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ==UserScript==
// @name Trakt.tv | Megascript
// @description My 15 trakt.tv userscripts merged into one for convenience: Actor Pronunciation Helper, All-In-One Lists View, Average Season And Episode Ratings, Bug Fixes And Optimizations, Charts - Ratings Distribution, Charts - Seasons, Custom Links (Watch-Now + External), Custom Profile Header Image, Enhanced List Preview Posters, Enhanced Title Metadata, Nested Header Navigation Menus, Partial VIP Unlock, Playback Progress Manager, Scheduled E-Mail Data Exports, Trakt API Wrapper. See README for details.
// @version 2026-03-24_14-23
// @namespace https://github.com/Fenn3c401
// @author Fenn3c401
// @license GPL-3.0-or-later
// @homepageURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection#readme
// @supportURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection/issues
// @updateURL https://update.greasyfork.org/scripts/557305.meta.js
// @downloadURL https://raw.githubusercontent.com/Fenn3c401/Trakt.tv-Userscript-Collection/main/userscripts/dist/zzzzzzzz.user.js
// @icon https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg
// @match https://trakt.tv/*
// @match https://classic.trakt.tv/*
// @run-at document-start
// @resource anidap https://anidap.se/logo.png
// @resource cineby https://www.cineby.gd/logo.png
// @resource dmm https://raw.githubusercontent.com/debridmediamanager/debrid-media-manager/main/dmm-logo.svg
// @resource hexa https://hexa.su/hexa-logo.png
// @resource knaben data:image/svg+xml,%3Csvg%20onmouseenter%3D%22this.querySelectorAll('%3Anth-child(-n%2B9)').forEach((c%2Ci)%3D%26gt%3B%7Bc.style.transition%3D'none'%3Bc.style.transform%3D'translate(0%2C-70%25)'%3BsetTimeout(()%3D%26gt%3B%7Bc.style.transition%3D'transform%201s%20cubic-bezier(.5%2C.25%2C.27%2C.1)'%3Bc.style.transform%3D'translate(0%2C0)'%7D%2C50*(i%253%2B~~(i%2F3)))%7D)%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201862%20804%22%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M1470.91%20273.76h280.14v100.1h-280.14z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M955.67%20273.76h499.85v100.1H955.67z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M653.56%20273.76h285.63v100.1H653.56z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M1470.91%20160.32h280.14v96.76h-280.14z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M955.67%20160.32h499.85v96.76H955.67z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M653.56%20160.32h285.63v96.76H653.56z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M1362.54%2040.2h281.94v101.77h-281.94z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M1062.98%2040.2h281.94v101.77h-281.94z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M763.42%2040.2h281.94v101.77H763.42z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M74.48%200h413.36v62.95H74.48zm0%2062.95h60.35v72.75H74.48zm136.41%200h37.2v72.75h-37.2zm107.47%200h37.2v72.75h-37.2zm111.61%200h57.87v72.75h-57.87zM74.48%20135.47h413.36v97.93H74.48z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M74.48%20233.16h502.74v140.7H74.48z%22%2F%3E%3Cpath%20fill%3D%22%23bababa%22%20d%3D%22M0%20391.991v.078L106.988%20644.12H1713.04v-2.908L1862%20492.251V391.95H.097Z%22%2F%3E%3Cpath%20fill%3D%22%237a7a7a%22%20d%3D%22M1713.489%20642.07H105.417l67.882%20159.92h1380.269Z%22%2F%3E%3C%2Fsvg%3E
// @resource kuroiru https://kuroiru.co/logo/stuff/letter-small.png
// @resource miruro https://www.miruro.to/assets/miruro-text-transparent-white-DRs0RmF1.png
// @resource oracleofbacon https://oracleofbacon.org/center_list.php
// @resource scenenzbs https://img.house-of-usenet.com/fd4bd542330506d41778e81860f29435c7f8795a7bbefbd9d297b7d79d5a067b.webp
// @resource stremio https://web.stremio.com/images/stremio_symbol.png
// @resource vidora data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzIgMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agc3RvcC1jb2xvcj0iIzAwZmY5ZCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzYwYTVmYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgcng9IjgiIGZpbGw9InVybCgjYSkiLz48cGF0aCBkPSJtOCA4IDggMTYgOC0xNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMy41Ii8+PC9zdmc+
// @require https://cdn.jsdelivr.net/gh/stdlib-js/[email protected]/browser.js#sha256-0SIsWI8h2EJjO46eyuxL1XnuGNhycW/o0yxyw/U+jrU=
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-zoom.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/croner.umd.min.js
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_getValue
// @grant GM_info
// @grant GM_openInTab
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_unregisterMenuCommand
// @grant GM.xmlHttpRequest
// @connect celeb.gate.cc
// @connect fanart.tv
// @connect forvo.com
// @connect kuroiru.co
// @connect moviemaps.org
// @connect trakt.tv
// @connect walter-r2.trakt.tv
// ==/UserScript==
/* README
### General
- You can disable individual modules by setting the corresponding id to `false` in the userscript storage tab *(note: only displayed after first run and with "Config mode" set to "Advanced" in TM settings)*.
- Each enabled module will conflict with the corresponding standalone userscript. Either uninstall the standalone version (suggested) or disable the respective module.
- Newly added modules will automatically get enabled when the script is updated. Modules that you already disabled will stay disabled.
- As VIP user you should disable: `2dz6ub1t`, `fyk2l3vj`, `x70tru7b`, `2hc6zfyy`
- This userscript is automatically generated. YMMV.
| *NAME* | *ID* |
| :----- | :---------- |
| [Trakt.tv \| Actor Pronunciation Helper](71cd9s61.md#StickyHeader "Adds a button on /people pages for fetching an audio recording of that person's name with the correct pronunciation from https://forvo.com") | `71cd9s61` |
| [Trakt.tv \| All-In-One Lists View](p2o98x5r.md#StickyHeader "Adds a button for appending your lists from the /collaborations, /liked and /liked/official pages on the main \"Personal Lists\" page for easier access and management of all your lists in one place. Essentially an alternative to the lists category dropdown menu.") | `p2o98x5r` |
| [Trakt.tv \| Average Season And Episode Ratings](yl9xlca7.md#StickyHeader "Shows the average general and personal rating of the seasons of a show and the episodes of a season. You can see the averages for all episodes of a show on its /seasons/all page.") | `yl9xlca7` |
| [Trakt.tv \| Bug Fixes And Optimizations](brzmp0a9.md#StickyHeader "A large collection of bug fixes and optimizations for trakt.tv, organized into ~35 independent sections, each with a comment detailing which specific issues are being addressed. Also contains some minor feature patches.") | `brzmp0a9` |
| [Trakt.tv \| Charts - Ratings Distribution](pmdf6nr9.md#StickyHeader "Adds a ratings distribution (number of users who rated a title 1/10, 2/10 etc.) chart to title summary pages. Also allows for rating the title by clicking on the bars of the chart.") | `pmdf6nr9` |
| [Trakt.tv \| Charts - Seasons](cs1u5z40.md#StickyHeader "Adds a line chart to /seasons pages which shows the ratings (personal + general) and the number of watchers and comments for each individual episode.") | `cs1u5z40` |
| [Trakt.tv \| Custom Links (Watch-Now + External)](wkt34fcz.md#StickyHeader "Adds custom links to all the \"Watch-Now\" and \"External\" sections (for titles and people). The ~35 defaults include Letterboxd, Stremio, streaming sites (e.g. P-Stream, Hexa), torrent aggregators (e.g. EXT, Knaben), various anime sites (both for streaming and tracking) and much more. Easily customizable.") | `wkt34fcz` |
| [Trakt.tv \| Custom Profile Header Image](2dz6ub1t.md#StickyHeader "A custom profile image for free users. Like the vip feature, except this one only works locally. Uses the native set/reset buttons and changes the dashboard + settings background as well.") | `2dz6ub1t` |
| [Trakt.tv \| Enhanced List Preview Posters](kji85iek.md#StickyHeader "Makes the posters of list preview stacks/shelves link to the respective title summary pages instead of the list page and adds corner rating indicators for rated titles.") | `kji85iek` |
| [Trakt.tv \| Enhanced Title Metadata](fyk2l3vj.md#StickyHeader "Adds links of filtered search results to the metadata section (languages, genres, networks, studios, writers, certification, year) on title summary pages, similar to the vip feature. Also adds a country flag and allows for \"combined\" searches by clicking on the labels.") | `fyk2l3vj` |
| [Trakt.tv \| Nested Header Navigation Menus](txw82860.md#StickyHeader "Adds 150+ dropdown menus with a total of 1000+ entries to the header navigation bar for one-click access to just about any page on the entire website.") | `txw82860` |
| [Trakt.tv \| Partial VIP Unlock](x70tru7b.md#StickyHeader "Unlocks some vip features: advanced filters, creation of new lists, \"more\" buttons on dashboard, faster page navigation, bulk list management, rewatching, custom calendars, advanced list progress and more. Also hides some vip advertisements.") | `x70tru7b` |
| [Trakt.tv \| Playback Progress Manager](swtn5c9q.md#StickyHeader "Adds playback progress badges to in-progress movies/episodes and allows for setting and removing playback progress states. Also adds playback progress overview pages to the \"Progress\" tab and allows for bulk deletion and renewal. DOES NOT WORK WITHOUT THE \"TRAKT API WRAPPER\" USERSCRIPT!") | `swtn5c9q` |
| [Trakt.tv \| Scheduled E-Mail Data Exports](2hc6zfyy.md#StickyHeader "OUT OF ORDER (for the time being). Automatic trakt.tv backups for free users. On every trakt.tv visit a background e-mail data export is triggered, if one is overdue based on the specified cron expression (defaults to weekly).") | `2hc6zfyy` |
| [Trakt.tv \| Trakt API Wrapper](f785bub0.md#StickyHeader "Exposes an authenticated Trakt API Wrapper. Intended to run alongside other userscripts which require (authenticated) access to the Trakt API.") | `f785bub0` |
*/
/* [Trakt.tv | Custom Profile Header Image]
A custom profile image for free users. Like the vip feature, except this one only works locally. Uses the native set/reset buttons and changes the dashboard + settings background as well.
*/
/* [Trakt.tv | Scheduled E-Mail Data Exports]
OUT OF ORDER (for the time being). Automatic trakt.tv backups for free users. On every trakt.tv visit a background e-mail data export is triggered, if one is overdue based on the specified cron expression (defaults to weekly).
### General
- You might want to consider the use of an e-mail filter, so as to e.g. automatically move the data export e-mails to a dedicated trakt-tv-data-exports folder.
- If you don't like the success toasts, you can turn them off by setting `toastOnSuccess` to `false` in the userscript storage tab *(note: only displayed after first run)*, there you can
also specify your own [cron expression](https://crontab.guru/examples.html). E-Mail data exports have a cooldown period of 24 hours, there is no point in going below that with your cron expression.
*/
/* [Trakt.tv | Actor Pronunciation Helper]
Adds a button on /people pages for fetching an audio recording of that person's name with the correct pronunciation from https://forvo.com
*/
/* [Trakt.tv | Bug Fixes And Optimizations]
A large collection of bug fixes and optimizations for trakt.tv, organized into ~35 independent sections, each with a comment detailing which specific issues are being addressed. Also contains some minor feature patches.
### General
- Please take a look at [the code](../dist/brzmp0a9.user.js) and glimpse over the comments for each section to get an idea as to what exactly you can expect from this script.
- Notably there are also a handful of feature patches included, all of them too minor to warrant a separate userscript:
- make the "add to list" buttons on grid pages (e.g. /trending) color-coded:<br>
[](#) = is on watchlist,
[](#) = is on personal list,
[](#) = is on both
- change the default sorting on /people pages from "released" to "popularity"
- grey out usernames of deleted profiles in the comments
- append `(@<userslug>)` to usernames in comments (Trakt allows users to set a "Display Name", separate from the username/slug. This becomes a problem in comment replies
which always reference the person/comment they are replying to with an `@<userslug>` prefix, which sometimes turns long reply chains into a game of matching pairs..), currently not supported in FF
- some custom hotkeys and gestures as displayed below
### Hotkeys/Gestures (Custom and Native)
- ***[CUSTOM]*** `alt + 1/2/3/4/5/6/7`: change header-search-category, 1 for "Shows & Movies", 2 for "Shows", ..., 7 for "Users", also expands header-search if collapsed
- ***[CUSTOM]*** `swipe in from left edge`: display title sidebar on mobile devices
- `meta(win)/ctrl + left click`: open in new tab instead of redirect (applies to header search results + "view watched history" button on title summary pages)
- `/`: expand header-search
- `w`: show filter-by-streaming-services modal
- `t`: show filter-by-terms modal
- `a`: toggle advanced-filters
- `m`: toggle manage-list mode (with item move, delete etc.)
- `r`: toggle reorder-lists mode (change list-rank on /lists page)
- `esc`: collapse header-search, hide popover, hide modal (check-in, watch-now, filter-by-terms)
- `enter`: redirect to selected header-search result, submit (advanced filters selection, date-time-picker input etc.)
- `ctrl + enter`: save note, submit comment
- `arrow-left/right OR p/n OR swipe right/left on fanart`: page navigation (e.g. prev/next episode, prev/next results page)
- `arrow-up/down`: header-search results navigation
*/
/* [Trakt.tv | Charts - Seasons]
Adds a line chart to /seasons pages which shows the ratings (personal + general) and the number of watchers and comments for each individual episode.
### General
- Clicking on the individual data points takes you to the summary page of the respective episode (or the comment page for comment data points).
- For charts with more than eight episodes, you can also zoom in by highlighting a section of the x-axis with your mouse. You can zoom out again by clicking anywhere inside the chart.
- This script won't work (well) on mobile devices and the chart is no beauty on light mode either. Basically the whole thing needs an overhaul and is not even close to being finished,
but the core functionality is there and it might be while until I get back to it, which is why I'm putting it out there as it is right now.
*/
/* [Trakt.tv | Trakt API Wrapper]
Exposes an authenticated Trakt API Wrapper. Intended to run alongside other userscripts which require (authenticated) access to the Trakt API.
> Based on iDavide94/iFelix18's [Trakt API wrapper for userscripts](https://github.com/iDavide94) which in turn was based on
> vankasteelj's [Trakt.tv API wrapper for Node.js](https://github.com/vankasteelj/trakt.tv).
### General
- If you're not interested in the technical details, then the rest of the documentation here will probably be of little interest to you. This script will mostly speed up some of
my other userscripts which would otherwise scrape the respective data instead of pulling it from the api (the api responses are cached for 8 hours).
- This wrapper is entirely self sufficient and requires no prior configuration or user input. It creates its own api application, fetches the respective client credentials and
authenticates the user on the fly when a method is called. You can change the name and description of that api application to whatever you like,
however please keep in mind that the name (in addition to the id) is used for checking for an existing application before creating a new one (useful after script reinstall).
If you delete it a new one will be created in its place when the wrapper is used again.
- Only works if a user is logged in.
- The methods were last updated in Jan. 2026.
### Usage
- The wrapper is exposed through `window.userscriptTraktApiWrapper` and can be used like:<br>
`const { data, meta } = await userscriptTraktApiWrapper.search.id({ id_type: 'trakt', id: 1234, type: 'episode', extended: 'full', _auth: true, _meta: true, _revalidate: true });`<br>
There are two types of props you can pass to a method, parameters corresponding to those listed in the Trakt API docs and options (denoted by a leading `_`) for the wrapper itself.
- ***Parameters***<br>
There are three categories: path parameters, search parameters, and the props for the request's body. First the mandatory and optional parameters for the path
are filled in, then for `GET`/`DELETE` requests the remaining parameters are appended as search parameters, whereas for `POST`/`PUT` requests they are added to the body.
- ***Options***<br>
- `_method` - The type of HTTP request to make, normally supplied by the method config, but as with all options you can override it.
- `_path` - The path template to use, normally supplied by the method config. Override it in case it's outdated.
- `_auth` - Whether to authenticate the request. Normally supplied by the method config for methods with mandatory authentication.
Override it to enable optional authentication (which also has a separate rate limit).
- `_meta` - Whether to wrap the response's data in an object with the supplied trakt header metadata like: `{ data: returned_data, meta: { pagination_page: 3 } }`.
`meta` includes all the trakt `x-`-prefixed headers and a couple select others in a normalized form to allow for dot-syntax access. Type coercion is done for numbers and booleans.
It's also included as `parsedTraktHeaders` with the raw response object which get's thrown in case of a failed request.
- `_revalidate` - Whether to revalidate the data instead of directly pulling it from the disk cache when possible.
- `_retry` - Configuration for a basic exponential backoff based retry mechanism. By default only activated for authed `POST`/`PUT`/`DELETE` requests.
Doesn't use the `ratelimit` and `retry_after` trakt headers. Takes a config object like `{ limit: 5, req_delay: 1000, resp_delay: 0 }`, with each retry
`limit` gets decremented (= 5 retries) and `req_delay` doubled. If you want to turn it off you can just override it with `_retry: null`.
- In the userscript storage tab you can change the `apiUrl` to `https://api-staging.trakt.tv` for a sandbox environment
and you can activate console logging of all api requests with `logApiRequests`.
- There's built-in rate-limiting for authed `POST`/`PUT`/`DELETE` requests (1/sec), which is complemented by the default `_retry` config, so you can just make a bunch of
requests at once and they'll be queued up and executed one by one in the same order in which you made them (retries will block the queue).
*/
/* [Trakt.tv | Enhanced Title Metadata]
Adds links of filtered search results to the metadata section (languages, genres, networks, studios, writers, certification, year) on title summary pages, similar to the vip feature. Also adds a country flag and allows for "combined" searches by clicking on the labels.
> Based on sergeyhist's [Trakt.tv Clickable Info](https://github.com/sergeyhist/trakt-scripts/blob/main/trakt-info.user.js) userscript.
### General
- By installing the [Trakt.tv | Trakt API Wrapper](f785bub0.md) userscript you can speed up the studios data fetching.
- By clicking on the label for languages, genres, networks, studios and writers, you can make a search for all their respective values combined, ANDed for genres, languages and writers,
ORed for networks and studios. For example if the genres are "Crime" and "Drama", then a label search will return a selection of other titles that also have the genres "Crime" AND "Drama".
- The writers label search was mostly added as an example of how to search for filmography intersections with trakt's search engine (there's no official tutorial about this,
just some vague one liner in the api docs about how `+ - && || ! ( ) { } [ ] ^ " ~ * ? : /` have "special meaning" when used in a query).
It's much more interesting with actors e.g. [Movies with Will Smith and Alan Tudyk](https://trakt.tv/search/movies?query=%22Will%20Smith%22+%22Alan%20Tudyk%22&fields=people).
- The title's certification links to the respective `/parentalguide` imdb page (which contains descriptions of nude scenes, graphic content etc.).
- The title's year links to the search page for other titles from the same year.
- The search results default to either the "movies" or "shows" search category depending on the type of the current title.
- A "+ n more" button is added for networks when needed (some anime have more than a dozen listed).
- Installing the [Trakt.tv | Partial VIP Unlock](x70tru7b.md) userscript will allow free users to further modify the applied advanced filters on the linked search pages.
- This script won't work for vip users.
*/
/* [Trakt.tv | Enhanced List Preview Posters]
Makes the posters of list preview stacks/shelves link to the respective title summary pages instead of the list page and adds corner rating indicators for rated titles.
### General
- The [Trakt.tv | Bug Fixes and Optimizations](brzmp0a9.md) userscript fixes some rating related issues and enables (more) reliable updates of the list-preview-poster rating indicators.
*/
/* [Trakt.tv | All-In-One Lists View]
Adds a button for appending your lists from the /collaborations, /liked and /liked/official pages on the main "Personal Lists" page for easier access and management of all your lists in one place. Essentially an alternative to the lists category dropdown menu.
### General
- Sorting, filtering and list actions (unlike, delete etc.) should work as usual. Also works on /lists pages of other users.
- The [Trakt.tv | Bug Fixes and Optimizations](brzmp0a9.md) userscript contains an improved/fixed `renderReadmore()` function (for "Read more/less..." buttons of long list descriptions),
which greatly speeds up the rendering of the appended lists.
*/
/* [Trakt.tv | Charts - Ratings Distribution]
Adds a ratings distribution (number of users who rated a title 1/10, 2/10 etc.) chart to title summary pages. Also allows for rating the title by clicking on the bars of the chart.
### General
- By installing the [Trakt.tv | Trakt API Wrapper](f785bub0.md) userscript you can speed up the ratings distribution data fetching.
*/
/* [Trakt.tv | Playback Progress Manager]
Adds playback progress badges to in-progress movies/episodes and allows for setting and removing playback progress states. Also adds playback progress overview pages to the "Progress" tab and allows for bulk deletion and renewal. DOES NOT WORK WITHOUT THE "TRAKT API WRAPPER" USERSCRIPT!
> Inspired by sharkykh's [Trakt.tv Playback Progress Manager](https://sharkykh.github.io/tppm/).
### General
- This script does not work without the [Trakt API Wrapper](f785bub0.md) userscript, so you'll need to install that one as well (or the [Megascript](zzzzzzzz.md)).
- By clicking on a playback progress badge, you can access options to either set a new playback progress state or remove it entirely.
- There are three context menu commands. "Set New" is only available on movie and episode summary pages and allows for setting a new playback progress state for that title.
"Delete All" and "Renew All" are only available on the [Playback Progress - All Types](https://trakt.tv/users/me/progress/playback) page as those affect all stored playback progress states.
From my testing the context menu commands are added reliably in Chrome, but not so much in Firefox. Fortunately Tampermonkey allows for triggering context menu commands via its
extension popup window as well (see the screenshots below), so you can just use that as alternative.
- Playback progress states are automatically removed by Trakt after 6 months. Renewing them postpones the auto-removal by first removing and then setting the
playback progress states again, while preserving the current order.
- Marking an in-progress movie or episode as watched will also remove the corresponding playback progress state.
### Playback Progress on Trakt
Trakt has supported storing playback progress states for movies and episodes via their api for many years now, however for some reason they never actually bothered to add support
for this to their website, so if you wanted to access those progress states you had to either do it through whichever 3rd party application saved them in the first place,
or use sharkykh's [TPPM](https://sharkykh.github.io/tppm/).
This has changed now, they've finally added native support for this to the new lite version of the website. Specifically on the "continue watching" page you can now see and remove
the playback progress states of movies. From what I can tell there's no episode support, no bulk actions, no option to set a new state and most importantly there are no
playback progress indicators on movie summary pages or any of the other grid views outside of the "continue watching" page. It's a rather lackluster implementation,
though at least it's in line with the rest of their new version.
*/
/* [Trakt.tv | Nested Header Navigation Menus]
Adds 150+ dropdown menus with a total of 1000+ entries to the header navigation bar for one-click access to just about any page on the entire website.
> Based on sergeyhist's [Trakt.tv Hidden Items](https://github.com/sergeyhist/trakt-scripts/blob/main/Legacy/trakt-hidden.user.js) userscript.
### General
- Amongst the added submenus is one called "Quick Actions" which allows for clearing the search history and re-caching progress and browser data. Usually those options are hidden in the advanced settings menu.
*/
/* [Trakt.tv | Custom Links (Watch-Now + External)]
Adds custom links to all the "Watch-Now" and "External" sections (for titles and people). The ~35 defaults include Letterboxd, Stremio, streaming sites (e.g. P-Stream, Hexa), torrent aggregators (e.g. EXT, Knaben), various anime sites (both for streaming and tracking) and much more. Easily customizable.
> Based on Tusky's [Trakt Watchlist Downloader](https://greasyfork.org/scripts/17991) with some sites/features/ideas borrowed from Accus1958's
> [trakt.tv Streaming Services Integration](https://greasyfork.org/scripts/486706), JourneyOver's [External links on Trakt](https://greasyfork.org/en/scripts/547223),
> sergeyhist's [Watch Now Alternative](https://github.com/sergeyhist/trakt-watch-now-alternative) and Tanase Gabriel's [Trakt.tv Universal Search](https://greasyfork.org/en/scripts/508020) userscripts.
### General
- Config options available via the userscript storage tab: *(note: only displayed after first run and with "Config mode" set to "Advanced" in TM settings)*
- `maxSidebarWnLinks`: Controls how many watch-now links are visible in the watch-now preview of the sidebar (defaults to `4`).
- `torrentResolution`: Resolution used for the query of the torrent and usenet links (defaults to `1080p`).
- `includeNsfwLinks`: Toggles the visibility of the NSFW links (defaults to `false` for greasyfork compliance).
- Usually watch-now buttons of grid-items are only displayed if the title has been released and is available for streaming in your selected watch-now country.
This script changes that by unhiding all watch-now buttons and color coding them as to the title's digital release status. White means the title is available for streaming
in your selected watch-now country, light-grey means the title is available for streaming in another country and dark-grey means that the title is not available for streaming anywhere.
- Nearly all links are direct links to e.g. individual episodes, as opposed to search links, anime included.
- There's a "fix" for anime which default to the "wrong" episode group (aka. "alternate seasons"). For example "Solo Leveling" is listed with its second season being part of the first,
and the episodes for "Cowboy Bebop" are all out of order, which would otherwise mess up direct linking to streaming sites. Trakt uses whichever grouping is used by TMDB and they have some,
to put it nicely, "questionable" and very much rigid rules regarding e.g. what exactly constitutes a season, the "Attack on Titan" finale being part of the specials is a prime example..
- Some urls are constructed dynamically on click. That means there might be a small delay before the page opens. The resolved url is then also set as href, so on a second click
the element behaves just like a regular link. A dynamic link is also resolved on right click, so you can e.g. do a double right click with a small delay in between
to use the "open in incognito window" option like you can with a regular link.
- Some links are configured to only be added if certain conditions are met, e.g. anime links are only added for titles where "anime" is included in the genres.
- A scrollable plot summary is added to the watch-now modal. The watch-now modal and the sidebar are also made scrollable.
- I only included anime streaming sites which used some sort of known external id (e.g. mal, anilist) and an episode number for their episode urls, to allow for direct linking.
One of these is "Kuroiru", an anime aggregator which contains more direct episode links to other popular anime streaming sites like HiAnime or AnimeKai.
### Default Custom Links
#### Watch-Now
- [EXT](https://ext.to) [Torrent Aggregator]
- [Stremio](https://www.stremio.com) [Debrid]
- [Knaben Database](https://knaben.org) [Torrent Aggregator]
- [Kuroiru](https://kuroiru.co) [Anime Aggregator]
- [AniDap](https://anidap.se) [Anime Streaming]
- [Miruro](https://www.miruro.to) [Anime Streaming]
- [Fmovies+](https://www.fmovies.gd) [Streaming]
- ~~[Vidora](https://watch.vidora.su) [Streaming] (embedded playback with support for one-way playback progress syncing if you've got my [PPM userscript](swtn5c9q.md) installed as well (only 5-80% and no scrobbling!))~~
- [Cineby](https://www.cineby.gd) [Streaming]
- [Hexa](https://hexa.su) [Streaming]
- [SceneNZBs](https://scenenzbs.com) [Usenet Indexer] (great fallback if a title is not available on public torrent trackers or streaming sites)
- [Debrid Media Manager](https://debridmediamanager.com) [Debrid]
#### External
- [Reddit](https://www.reddit.com) (discussions)
- [Letterboxd](https://letterboxd.com) (popular movie tracking site; lots of users, lists and reviews)
- [ReverseTV](https://reversetv.enzon19.com) ("Where have I seen each cast member before?")
- [MovieMaps](https://moviemaps.org) (interactive map of filming locations)
- [Fandom](https://www.fandom.com) (fan-made encyclopedias)
- [AZNude](https://www.aznude.com) (NSFW; nude scenes for titles and people)
- [CelebGate](https://celeb.gate.cc) (NSFW; people only; great source for leaks)
- [Rule 34](https://rule34.xxx) (NSFW; titles only; "If it exists there is p*rn of it.")
- [MyAnimeList](https://myanimelist.net) (anime tracking site)
- [AniList](https://anilist.co) (anime tracking site)
- [AniDB](https://anidb.net) (anime tracking site)
- [LiveChart](https://www.livechart.me) (anime tracking site)
- [TheTVDB](https://thetvdb.com) (similar to TMDB and IMDb)
- [TVmaze](https://www.tvmaze.com) (tv show tracking site)
- [YouTube Trailer](https://www.youtube.com) (season trailers are preferred)
- [YouTube Interviews](https://www.youtube.com) (e.g. actors in talkshows)
- [Rotten Tomatoes](https://www.rottentomatoes.com) (ratings/reviews from professional critics)
- [Metacritic](https://www.metacritic.com) (ratings/reviews from professional critics)
- [Spotify](https://open.spotify.com) (soundtracks)
- [MediUX](https://mediux.pro) (similar to fanart.tv)
- [YouGlish](https://youglish.com) ("How do I pronounce this actor's name?")
- [Oracle of Bacon](https://oracleofbacon.org) ([Six Degrees of Kevin Bacon](https://en.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon))
*/
/* [Trakt.tv | Partial VIP Unlock]
Unlocks some vip features: advanced filters, creation of new lists, "more" buttons on dashboard, faster page navigation, bulk list management, rewatching, custom calendars, advanced list progress and more. Also hides some vip advertisements.
### Full Unlock
- ***"more" buttons on dashboard***
- ***~2x faster page navigation with Hotwire's Turbo***<br>
(Allows for partial page updates instead of full page reloads when navigating, might break userscripts from other devs who didn't account for this.
Also imo it's nothing short of embarassing for them to think it's good idea to intentionally slow down their website for free users. There's a reason they don't have it listed amongst the vip perks..)
- ***rewatching***
- ***vip badge***<br>
(Appends a special "Director" badge to your username. It's usually reserved for team members like Trakt's co-founders Sean and Justin. See https://trakt.tv/users/sean for how it looks.)
- ***all vip settings from the `/settings` page***<br>
(calendar autoscroll, limit dashboard "up next" episodes to watch-now favorites, only show watch-now icon if title is available on favorites, rewatching settings)
- ***filter-by-terms***
- ***watch-now modal country selection***
### Partial Unlock
- ***advanced filters***<br>
(You can save filter presets to the sidebar with the "Save Filters" context menu command (available via Tampermonkey's extension popup window as well).)
- ***custom calendars***<br>
(get generated and work, but are not listed in sidebar and can't be deleted, so you have to save the url of the custom calendar or "regenerate" it on the `/lists` page)
- ***advanced list progress***<br>
(From my understanding the idea is to filter your `/progress/watched` and `/progress/dropped` pages by the shows on a specific list. As this script also unlocks
the filter-by-terms function which on the `/progress` pages happens to have regex support, it's possible to just OR all titles of watched shows on a list to get the same result.
Drawbacks of this are that you can't use filter-by-terms anymore, active filters are turned off in the process (e.g. hide completed), and that shows with the same name can lead to incorrect results.)
- ***bulk list copy and delete***<br>
(The "move" bulk list action is also unlocked but [can fail and result in data loss](https://greasyfork.org/en/scripts/557305-trakt-tv-megascript/discussions/320717),
which is why I removed the respective ui elem. If you want to make a bulk move you can instead first do a bulk copy and then a bulk deletion of the source list (same result but much safer).<br>
The item selection is filter based, so if you're filtering a list by genre then the bulk list actions will only apply to titles with that genre. Filtering by trakt-rating, trakt-votes,
years and runtime works as well, just directly modify the search params in the url (it's the same as for the advanced filters).)
- ***creation of new lists***<br>
(You can bypass the limit for the amount of lists a free user is allowed to have, by going to any existing list (doesn't have to be your own) with 1-100 items,
and then using the "copy to new list" option and it will create a new list for you, which you can then edit and use however you want.
The "copied from..." text is not added if you use one of your own lists as source (like your favorites).)
- ***~~adding items to maxed-out lists~~ => They unfortunately fixed that.***<br>
(This bypass was discovered thanks to an issue from [SET19724](https://github.com/SET19724). You can now add titles to maxed-out lists with the regular ui elements, which will trigger
two background bulk move ops because those don't properly enforce the max item limit for lists. Per 1000 items on the target list this will take ~45s. In the same way
you can also merge lists manually. Say you've got the lists: `watchlist1` + `watchlist2` with 99 items each and `watchlist3` with 100 items. You can now do a bulk move
from `watchlist3` to `watchlist2`, followed by a bulk move from `watchlist2` to `watchlist1`, to accumulate all 298 items on that list.
So you can grow lists to a max-size of ~4100 items by sequentially merging them with target lists that have <= 99 items.)
- ***~~rss/ical feeds + csv exports~~ => [Trakt was leaking private user data](https://www.reddit.com/r/Addons4Kodi/comments/1rklk67/trakt_was_leaking_private_user_data/)***<br>
### Semi-Private Notes in Comments
Trakt supports markdown syntax in comments, including reference-style links which you can misuse as a semi-private notes container like `[//]: # (hidden text goes here)`.
The raw markdown is of course still accessible to anyone through the Trakt api and the `/comments/<comment-id>.json` endpoint (you yourself can also see the raw version when editing),
but the content is not rendered in the classic and new web versions, in fact a comment can appear to be completely empty this way. I think this is interesting because it's a relatively elegant way
to work around the max. limit for private notes (currently 100), as the note-comments are still stored directly on your Trakt account on a per-title basis and can easily be accessed on arbitrary
platforms, including ones that don't support userscripts. It's probably advisable to disguise the note-comments by always adding some generic one-liner.
### Filter-By-Terms Regex
The filter-by-terms (also called "Filter by Title") function works either server or client-side, depending on whether the exact place you're using it from is paginated or not.
The `/users/<userslug>/lists`, `/seasons` and `/people` pages are all not paginated, so there the filtering is done client-side, with the input being interpreted as
a case-insensitive regular expression. All other places where the filter-by-terms function is available are paginated and therefore use server-side filtering,
those usually don't allow for regular expressions, with the exception of the `/progress` page and list pages. The input is matched against:
- list title and description for `/users/<userslug>/lists` pages
- episode title for `/seasons` pages
- title and character name for `/people` pages
- episode and show title for `/progress` pages
- title name for list pages
*/
/* [Trakt.tv | Average Season And Episode Ratings]
Shows the average general and personal rating of the seasons of a show and the episodes of a season. You can see the averages for all episodes of a show on its /seasons/all page.
> Based on Tusky's [Trakt Average Season Rating](https://greasyfork.org/scripts/30728) userscript.
### General
- The general ratings average is weighted by votes, to account for the inaccurate ratings of unreleased seasons/episodes.
- Specials are always excluded, except on the specials season page.
- Only visible (i.e. not hidden by a filter) items are used for the calculation of the averages and changes to those filters trigger a recalculation.
*/
'use strict';
const gmStorage = { '2dz6ub1t': true, '2hc6zfyy': true, '71cd9s61': true, 'brzmp0a9': true, 'cs1u5z40': true, 'f785bub0': true, 'fyk2l3vj': true, 'kji85iek': true, 'p2o98x5r': true, 'pmdf6nr9': true, 'swtn5c9q': true, 'txw82860': true, 'wkt34fcz': true, 'x70tru7b': true, 'yl9xlca7': true, ...(GM_getValue('megascript')) };
GM_setValue('megascript', gmStorage);
gmStorage['2dz6ub1t'] && (async (moduleName) => {
/* global moduleName */
'use strict';
let $;
const logger = {
_defaults: {
title: (typeof moduleName !== 'undefined' ? moduleName : GM_info.script.name).replace('Trakt.tv', 'Userscript'),
toast: true,
toastrOpt: { positionClass: 'toast-top-right', timeOut: 10000, progressBar: true },
toastrStyles: '#toast-container#toast-container a { color: #fff !important; border-bottom: dotted 1px #fff; }',
},
_print(fnConsole, fnToastr, msg = '', opt = {}) {
const { title = this._defaults.title, toast = this._defaults.toast, toastrOpt, toastrStyles = '', consoleStyles = '', data } = opt,
fullToastrMsg = `${msg}${data !== undefined ? ' See console for details.' : ''}<style>${this._defaults.toastrStyles + toastrStyles}</style>`;
console[fnConsole](`%c${title}: ${msg}`, consoleStyles, ...(data !== undefined ? [data] : []));
if (toast) unsafeWindow.toastr?.[fnToastr](fullToastrMsg, title, { ...this._defaults.toastrOpt, ...toastrOpt });
},
info(msg, opt) { this._print('info', 'info', msg, opt) },
success(msg, opt) { this._print('info', 'success', msg, { consoleStyles: 'color:#00c853;', ...opt }) },
warning(msg, opt) { this._print('warn', 'warning', msg, opt) },
error(msg, opt) { this._print('error', 'error', msg, opt) },
};
const gmStorage = { ...(GM_getValue('customProfileImage')) };
GM_setValue('customProfileImage', gmStorage);
let styles = addStyles();
window.addEventListener('turbo:load', () => {
if (!/^\/(shows|movies|users|dashboard|settings|oauth\/(authorized_)?applications)/.test(location.pathname)) return;
$ ??= unsafeWindow.jQuery;
if (!$) return;
const $coverWrapper = $('body.is-self #cover-wrapper'),
$btnSetProfileImage = $('body.is-self #btn-set-profile-image'),
$fullScreenshot = $('body:is(.shows, .movies) #summary-wrapper > .full-screenshot');
if (gmStorage.imgUrl && $coverWrapper.length && $btnSetProfileImage.length) addUserPageElems($coverWrapper, $btnSetProfileImage);
if ($fullScreenshot.length) {
if ($fullScreenshot.attr('style')) addTitlePageElems($fullScreenshot);
else {
new MutationObserver((_muts, mutObs) => {
mutObs.disconnect();
addTitlePageElems($fullScreenshot);
}).observe($fullScreenshot[0], { attributeFilter: ['style'] }); // native logic for selection of bg img (fanart vs screenshot) is quite complex
}
}
});
function addUserPageElems($coverWrapper, $btnSetProfileImage) {
if ($coverWrapper.has('a.selected:contains("Profile")').length) {
$coverWrapper.removeClass('slim')
.find('> .poster-bg-wrapper').removeClass('poster-bg-wrapper').addClass('shade');
if (!$coverWrapper.find('> #watching-now-wrapper').length) {
$coverWrapper.find('> .container').before(
`<div class="hidden-xs" id="fanart-info">` +
`<a href="${gmStorage.info.url}">${gmStorage.info.title} <span class="year">${gmStorage.info.year}</span></a>` +
`</div>`
);
}
} else {
$coverWrapper.find('> .poster-bg-wrapper').removeClass('poster-bg-wrapper').addClass('shadow-full-width');
}
$btnSetProfileImage.popover('destroy').popover({
trigger: 'manual',
container: 'body',
placement: 'bottom',
html: true,
template:
`<div class="popover remove reset-profile-image" role="tooltip">` +
`<div class="arrow"></div>` +
`<h3 class="popover-title"></h3>` +
`<div class="popover-content"></div>` +
`</div>`,
title: 'Reset Profile Image?',
content:
`<button class="btn btn-primary less-rounded">Yes</button>` +
`<button class="btn btn-cancel less-rounded" onclick="$(this).closest('.popover').popover('hide');">No</button>`,
}).on('click', function() { $(this).popover('show'); })
.find('.btn-text').text('Reset Profile Image');
$('body').on('click', '.reset-profile-image .btn-primary', () => {
['imgUrl', 'info'].forEach((prop) => delete gmStorage[prop]);
GM_setValue('customProfileImage', gmStorage);
styles?.remove();
logger.success('Custom profile image has been reset.');
$btnSetProfileImage.popover('destroy').popover({
trigger: 'hover',
container: 'body',
placement: 'bottom',
html: true,
template:
`<div class="popover set-profile-image" role="tooltip">` +
`<div class="arrow"></div>` +
`<h3 class="popover-title"></h3>` +
`<div class="popover-content"></div>` +
`</div>`,
content:
`Showcase your favorite movie, show, season or episode and make it your profile header image! Here's how:<br><br>` +
`<ol>` +
`<li>Go to any <b>movie</b>, <b>show</b>, <b>season</b>, or <b>episode</b> page.</li>` +
`<li>Click <b>Set Profile Image</b> in the sidebar.</li>` +
`</ol>`,
}).off('click')
.find('.btn-text').text('Set Profile Image');
$coverWrapper.addClass('slim')
.find('> :is(.shade, .shadow-full-width)').removeClass('shade shadow-full-width').addClass('poster-bg-wrapper')
.end().find('> #fanart-info').remove();
});
}
function addTitlePageElems($fullScreenshot) {
const fanartUrl = $fullScreenshot.css('background-image').match(/url\("?(?!.+?placeholders)(.+?)"?\)/)?.[1],
$setProfImgBtns = $('[href="/vip/cover"]');
const deactivateSetProfImgBtns = (reasonId) => {
$setProfImgBtns.has('.fa')
.parent().addClass('locked')
.find('.text').unwrap()
.append(`<div class="under-action">${['No fanart available', 'Already set'][reasonId]}</div>`);
$setProfImgBtns.not(':has(.fa)')
.off('click').on('click', (evt) => evt.preventDefault())
.css({ 'color': '#bbb' })
.find('.text').wrap('<s></s>');
};
if (!fanartUrl) deactivateSetProfImgBtns(0);
else if (fanartUrl === gmStorage.imgUrl) deactivateSetProfImgBtns(1);
else {
$setProfImgBtns.on('click', (evt) => {
evt.preventDefault();
deactivateSetProfImgBtns(1);
gmStorage.imgUrl = fanartUrl;
gmStorage.info = {
url: location.pathname,
title: $('head title').text().match(/(.+?)(?: \([0-9]{4}\))? - Trakt/)[1],
year: $('#summary-wrapper .year').text(),
};
GM_setValue('customProfileImage', gmStorage);
styles?.remove();
styles = addStyles();
logger.success('Fanart is now set as custom profile image. Click here to see how it looks.', { toastrOpt: { onclick() { location.href = '/users/me'; } } });
});
}
}
function addStyles() {
if (gmStorage.imgUrl) {
return GM_addStyle(`
body.users.is-self #cover-wrapper:not(:has(> #watching-now-wrapper)) > .full-bg {
background-image: url("${gmStorage.imgUrl}") !important;
}
@media (width <= 767px) and (orientation: portrait) {
body.users.is-self #cover-wrapper:not(:has(> #watching-now-wrapper)) > .container {
background-color: revert !important;
}
}
body:is(.dashboard, .settings, .authorized_applications, .applications) #results-top-wrapper .poster-bg {
background-image: url("${gmStorage.imgUrl}") !important;
background-size: cover !important;
background-position: 50% 20% !important;
opacity: 0.7 !important;
filter: revert !important;
}
`);
}
}
})('Trakt.tv | Custom Profile Header Image');
gmStorage['2hc6zfyy'] && (async (moduleName) => {
/* global moduleName, Cron */
'use strict';
let $, userslug;
const logger = {
_defaults: {
title: (typeof moduleName !== 'undefined' ? moduleName : GM_info.script.name).replace('Trakt.tv', 'Userscript'),
toast: true,
toastrOpt: { positionClass: 'toast-top-right', timeOut: 10000, progressBar: true },
toastrStyles: '#toast-container#toast-container a { color: #fff !important; border-bottom: dotted 1px #fff; }',
},
_print(fnConsole, fnToastr, msg = '', opt = {}) {
const { title = this._defaults.title, toast = this._defaults.toast, toastrOpt, toastrStyles = '', consoleStyles = '', data } = opt,
fullToastrMsg = `${msg}${data !== undefined ? ' See console for details.' : ''}<style>${this._defaults.toastrStyles + toastrStyles}</style>`;
console[fnConsole](`%c${title}: ${msg}`, consoleStyles, ...(data !== undefined ? [data] : []));
if (toast) unsafeWindow.toastr?.[fnToastr](fullToastrMsg, title, { ...this._defaults.toastrOpt, ...toastrOpt });
},
info(msg, opt) { this._print('info', 'info', msg, opt) },
success(msg, opt) { this._print('info', 'success', msg, { consoleStyles: 'color:#00c853;', ...opt }) },
warning(msg, opt) { this._print('warn', 'warning', msg, opt) },
error(msg, opt) { this._print('error', 'error', msg, opt) },
};
const gmStorage = { cronExpr: '@weekly', toastOnSuccess: true, lastRun: {}, ...(GM_getValue('scheduledEmailDataExports')) };
GM_setValue('scheduledEmailDataExports', gmStorage);
let cron;
try {
cron = new Cron(gmStorage.cronExpr, {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
} catch (err) {
logger.error('Invalid cron expression. Exiting..', { data: err });
}
false && cron && window.addEventListener('turbo:load', async () => {
$ ??= unsafeWindow.jQuery;
userslug ??= unsafeWindow.Cookies?.get('trakt_userslug');
if (!$ || !userslug) return;
const dateNow = new Date();
if (!gmStorage.lastRun[userslug] || cron.nextRun(gmStorage.lastRun[userslug]) <= dateNow) {
const realLastRun = await fetch('/settings/data').then((r) => r.text())
.then((r) => $(new DOMParser().parseFromString(r, 'text/html')).find('#exporters .alert-success .format-date').attr('data-date'));
if (realLastRun && cron.nextRun(realLastRun) > dateNow) {
gmStorage.lastRun[userslug] = realLastRun;
GM_setValue('scheduledEmailDataExports', gmStorage);
return;
}
$.post('/settings/export_data').done(() => {
gmStorage.lastRun[userslug] = dateNow.toISOString();
GM_setValue('scheduledEmailDataExports', gmStorage);
logger.success('Success. Your data export is processing. You will receive an e-mail when it is ready.', { toast: gmStorage.toastOnSuccess });
}).fail((xhr) => {
if (xhr.status === 409) {
gmStorage.lastRun[userslug] = dateNow.toISOString();
GM_setValue('scheduledEmailDataExports', gmStorage);
logger.warning(`Failed. Cooldown from previous export is still active. Will retry on next scheduled data export at: ${cron.nextRun(gmStorage.lastRun[userslug]).toISOString()}`);
} else {
logger.error(`Failed with status: ${xhr.status}. Reload page to try again.`, { data: xhr });
}
});
}
});
})('Trakt.tv | Scheduled E-Mail Data Exports');
gmStorage['71cd9s61'] && (async (moduleName) => {
/* global moduleName */
'use strict';
let $;
const logger = {
_defaults: {
title: (typeof moduleName !== 'undefined' ? moduleName : GM_info.script.name).replace('Trakt.tv', 'Userscript'),
toast: true,
toastrOpt: { positionClass: 'toast-top-right', timeOut: 10000, progressBar: true },
toastrStyles: '#toast-container#toast-container a { color: #fff !important; border-bottom: dotted 1px #fff; }',
},
_print(fnConsole, fnToastr, msg = '', opt = {}) {
const { title = this._defaults.title, toast = this._defaults.toast, toastrOpt, toastrStyles = '', consoleStyles = '', data } = opt,
fullToastrMsg = `${msg}${data !== undefined ? ' See console for details.' : ''}<style>${this._defaults.toastrStyles + toastrStyles}</style>`;
console[fnConsole](`%c${title}: ${msg}`, consoleStyles, ...(data !== undefined ? [data] : []));
if (toast) unsafeWindow.toastr?.[fnToastr](fullToastrMsg, title, { ...this._defaults.toastrOpt, ...toastrOpt });
},
info(msg, opt) { this._print('info', 'info', msg, opt) },
success(msg, opt) { this._print('info', 'success', msg, { consoleStyles: 'color:#00c853;', ...opt }) },
warning(msg, opt) { this._print('warn', 'warning', msg, opt) },
error(msg, opt) { this._print('error', 'error', msg, opt) },
};
addStyles();
document.addEventListener('turbo:load', () => {
if (!/^\/people\/[^\/]+(\/lists.*)?$/.test(location.pathname)) return;
$ ??= unsafeWindow.jQuery;
if (!$) return;
$(`<button id="btn-pronounce-name">` +
`<div class="audio-animation fade">` +
`<div class="bar-1"></div>` +
`<div class="bar-2"></div>` +
`<div class="bar-3"></div>` +
`</div>` +
`<div class="fa fa-volume fade in"></div>` +
`</button>`
).appendTo($('#summary-wrapper .mobile-title h1')).tooltip({
title: 'Pronounce Name',
container: 'body',
placement: 'top',
html: true,
}).one('click', async function() {
$(this).tooltip('hide');
const $btnPronounceName = $(this),
name = $('body > [itemtype$="Person"] > meta[itemprop="name"]').attr('content') ?? $('#summary-wrapper .mobile-title > :last-child').text(); // fallback for /people/<slug>/lists pages
unsafeWindow.showLoading?.();
const fullNameAudio = await fetchAudio(name);
const audios = fullNameAudio ? [fullNameAudio] : await Promise.all(name.split(/\s+/).map((namePart) => {
return /^\w\.?$/.test(namePart) ? new SpeechSynthesisUtterance(namePart) : fetchAudio(namePart).then((res) => res ?? new SpeechSynthesisUtterance(namePart));
}));
unsafeWindow.hideLoading?.();
if (audios.some((audio) => audio instanceof SpeechSynthesisUtterance)) {
audios.forEach((audio) => { if (audio instanceof SpeechSynthesisUtterance) audio.lang = 'en-US'; });
logger.warning(`Could not find a full pronunciation for "${name}" on ` +
`<a href="https://forvo.com/search/${encodeURIComponent(name)}" target="_blank"><strong>forvo.com</strong></a>. Falling back to TTS..`);
}
['ended', 'end'].forEach((type) => {
audios.slice(1).forEach((audio, i) => {
audios[i]?.addEventListener(type, () => audio.play ? audio.play() : speechSynthesis.speak(audio));
});
audios.at(-1).addEventListener(type, () => {
$btnPronounceName.find('.audio-animation').removeClass('in');
setTimeout(() => $btnPronounceName.find('.fa').addClass('in'), 150);
});
});
playAudios(audios, $btnPronounceName);
$btnPronounceName.on('click', () => playAudios(audios, $btnPronounceName));
});
}, { capture: true });
async function fetchAudio(query) {
const resp = await GM.xmlHttpRequest({ url: `https://forvo.com/search/${encodeURIComponent(query)}` }),
doc = new DOMParser().parseFromString(resp.responseText, 'text/html'),
audioHttpHost = $(doc).find('body > script').text().match(/_AUDIO_HTTP_HOST='(.+?)'/)?.[1],
audioPathsRaw = $(doc).find('[onclick^="Play"]').attr('onclick')?.match(/Play\([0-9]+,'(.*?)','(.*?)',(?:true|false),'(.*?)','(.*?)'/)?.slice(1),
audioPaths = audioPathsRaw?.map((pathRaw, i) => pathRaw && ['/mp3/', '/ogg/', '/audios/mp3/', '/audios/ogg/'][i] + atob(pathRaw)).filter(Boolean).reverse();
return audioPaths?.length ? $('<audio>' + audioPaths.map((path) => {
return `<source src="https://${audioHttpHost}${path}" type="${path.endsWith('mp3') ? 'audio/mpeg' : 'audio/ogg; codecs=vorbis'}" />`;
}).join('') + '</audio>')[0] : null;
}
function playAudios(audios, $btnPronounceName) {
$btnPronounceName.find('.fa').removeClass('in');
setTimeout(() => {
$btnPronounceName.find('.audio-animation').addClass('in');
audios.forEach((audio) => audio.load?.()); // for repeated playback; currentTime = 0 doesn't work for some audio files
speechSynthesis.cancel();
audios[0].play ? audios[0].play() : speechSynthesis.speak(audios[0]);
}, 150);
}
function addStyles() {
GM_addStyle(`
#btn-pronounce-name {
margin: 0 0 2px 7px;
position: relative;
height: 20px;
width: 20px;
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
border-style: none;
background-color: transparent;
}
#btn-pronounce-name .fa {
position: absolute;
font-size: 16px;
color: #aaa;
}
#btn-pronounce-name:hover .fa {
color: var(--link-color);
}
#btn-pronounce-name .audio-animation {
position: absolute;
height: 75%;
width: 75%;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
}
#btn-pronounce-name .audio-animation [class^="bar-"] {
flex: 1;
height: 100%;
border-radius: 3px;
background: linear-gradient(180deg, rgb(255 0 0), rgb(155 66 200));
transform: scaleY(0.2);
}
#btn-pronounce-name .in .bar-1 { animation: lineWave-1 .4s .3s infinite alternate; }
#btn-pronounce-name .in .bar-2 { animation: lineWave-2 .3s .2s infinite alternate; }
#btn-pronounce-name .in .bar-3 { animation: lineWave-3 .35s .25s infinite alternate; }
@keyframes lineWave-1 { from { transform: scaleY(0.24); } to { transform: scaleY(0.85); } }
@keyframes lineWave-2 { from { transform: scaleY(0.27); } to { transform: scaleY(0.98); } }
@keyframes lineWave-3 { from { transform: scaleY(0.24); } to { transform: scaleY(0.80); } }
`);
}
})('Trakt.tv | Actor Pronunciation Helper');
gmStorage['brzmp0a9'] && (async (moduleName) => {
/* BUG REPORTS
- items in "most watched shows and movies" section on profile page lack data-source-counts and data-source-slugs attrs for watch-now modal
- progress > dropped > toggle grid-view button => triggers GET /settings/grid_view/progress_dropped/1 or /0 and results in 400 resp but works for watched and collected
- There's no safeguarding against naming personal lists either "liked" or "collaborations". Leads to inaccessible lists, as list url (even when using id) always points to e.g. /lists/liked
- ratings distribution data is sometimes malformed (length of 11 with first index having value 1/2) e.g. /movies/oppenheimer-2023/stats, only affects movs + shows
- trakt api studio slugs only work with /search?studios= if no hyphens are included (meaning studio name is one single word) (why include slugs in response at all if deprecated?)
- network info in .additional-stats of /seasons/all pages is invalid, seems to always be some memory address instead of the actual network name, only affects free users
<li class="stat"><label>Networks</label>#<Network:0x00007fcf800876c0>, #<Network:0x00007fcf80087580>, #<Network:0x00007fcf80087440></li>
- "view progress" data for alternate seasons is nonsense (can be e.g. some random special episode or the show itself, not sure about the pattern) /shows/attack-on-titan/seasons/alternate/2848
- /discover/comments/reviews/lists /shouts/lists and /all/lists all return the same list comments
- somehow ascending/descending sort directions seem to consistently be inverted across the entire website
- The switch between mobile and tablet layout is controlled with media queries like max-width: 767px and min-width: 768px, which cause the page layout to break at a window width of
767px < width < 768px. At least in Chrome there's no rounding to the nearest integer. A switch to operators like <= and > would cover that edge case as well.
- "Progress" -> "Dropped" 1. doesn't allow for sorting by dropped date 2. still has a working "drop show" button despite shows already having been dropped
- "allow comments" setting of lists can be bypassed with manual post request (/comments page is available regardless of setting + this even works for deleted user profiles)
- appending a second date (which one doesn't matter) to the /shows-movies calendar url like /calendars/my/shows-movies/2024-10-14/2024-10-16 returns a view for all days until 2030
- "var words = characters.split(' ').length" comment word-counter is incorrect, needs e.g. .filter(Boolean) added for correct word count in case of consecutive spaces or empty text input
- click on streaming service button, after having filtered a list by streaming service, results in rangeError + stuck on loading
- calendar start/end date popover can't be hidden by clicking on icon again
- $.each(['following', 'following_pending', 'followers'] ... ends up checking last_activities for account['followers_at'] which does not exist as prop is called followed_at,
meaning when a new person follows you, this is not immediately reflected by the buttons in the network tab
*/
'use strict';
// FINISHED
/////////////////////////////////////////////////////////////////////////////////////////////
// - styles the header-search scrollbar
// - prevents overlap of long header-search queries from the "recent searches" section with the respective remove-from-search-history button
// - prevents the focused header-search bar from overlapping with the profile icon and the page's scrollbar on mobile layout
GM_addStyle(`
#header-search-autocomplete {
scrollbar-color: #666 transparent;
}
#header-search-autocomplete .search-term {
overflow: clip;
text-overflow: ellipsis;
}
#header-search-autocomplete .search-term > .in-type {
display: inline-block;
}
@media (width <= 767px) {
#top-nav .search-wrapper.focused {
z-index: 1;
}
#top-nav {
container-type: inline-size;
}
#top-nav .search-wrapper.focused #header-search#header-search {
width: 100cqi !important;
margin-left: -47px !important;
}
}
`);
// By default the category selection and advanced-filters sidenavs of grid views do not play well with window resizing. Based on the initial window size several fixed height and min-height
// inline styles get set, which can break the page layout in numerous ways, e.g. a large empty space above or below the .grid-items after resizing.
// Then there's some quirky scrolling behavior in the advanced-filters sidenav, the three "votes" sliders at the bottom get cut off on mobile-layout, the category sidenav's sticky positioning
// doesn't always work, there's some text overlap, some text is cut off, some missing padding, the display prop of the sidenav links is not adaptive and a bunch of other problems.
GM_addStyle(`
.frame-wrapper :is(.sidenav, .sidenav-inner) {
height: revert !important;
min-height: revert !important;
}
.frame-wrapper .sidenav .sidenav-inner {
position: revert !important;
}
.frame-wrapper #filter-fade-hide .dropdown-menu {
overflow-y: auto;
max-height: calc(100dvh - var(--header-height) - 55px);
scrollbar-width: thin;
scrollbar-color: #666 #333;
}
@media (width <= 1024px) {
.frame-wrapper .sidenav.advanced-filters {
padding: 10px 10px 75px !important;
top: 110px !important;
scrollbar-width: none;
}
.frame-wrapper .sidenav.advanced-filters .sidenav-inner {
max-height: revert !important;
}
.frame-wrapper .sidenav:not(.advanced-filters) nav .link:not([style="display: none;"]) {
display: inline !important;
}
}
@media (1024px < width) {
.frame-wrapper:has(> .sidenav.advanced-filters.open) {
background: linear-gradient(to right, #1d1d1d 300px, #222 300px 600px, #1d1d1d 600px) !important;
}
.frame-wrapper .frame {
display: flow-root;
margin-right: 0 !important;
min-height: calc(100dvh - var(--header-height));
}
.frame-wrapper .frame .no-results {
transform: revert !important;
}
.frame-wrapper .frame .personal-list .posters {
min-width: max-content;
}
.frame-wrapper .sidenav {
position: sticky !important;
top: 0;
}
.frame-wrapper .sidenav .sidenav-inner {
max-height: 100dvh;
}
.frame-wrapper .sidenav:not(.advanced-filters) {
z-index: 26;
}
.frame-wrapper .sidenav:not(.advanced-filters) .sidenav-inner {
display: flex;
flex-direction: column;
}
.frame-wrapper .sidenav:not(.advanced-filters) nav {
margin-top: 0 !important;
overflow-y: auto;
scrollbar-width: none;
mask: linear-gradient(to top, transparent, white 8px);
}
.frame-wrapper .sidenav:not(.advanced-filters) nav h3 {
position: sticky !important;
top: 0;
z-index: 1;
margin-bottom: 0 !important;
padding: 15px 0 10px !important;
background-color: #1d1d1d;
mask: linear-gradient(to top, transparent, white 8px);
}
.frame-wrapper .sidenav:not(.advanced-filters) nav .link.saved-filter {
margin-bottom: 10px !important;
padding-left: 7px;
}
.frame-wrapper .sidenav:not(.advanced-filters) nav .link:not([style*="display: none;"]) {
display: block !important;
}
.frame-wrapper .sidenav:not(.advanced-filters) .sidenav-inner > span {
display: none;
}
}
@media (991px < width <= 1024px) {
.frame-wrapper #filter-fade-hide .dropdown-menu {
right: 0;
left: revert !important;
}
}
`);
// Makes the imdb external rating link point to the title's main imdb page instead of its /ratings page,
// both because it's arguably the more relevant one and for consistency with the other external rating links (and because u/FatKitty asked for it).
document.addEventListener('turbo:load', () => {
if (/^\/(movies|shows)/.test(location.pathname)) {
unsafeWindow.jQuery?.('#summary-ratings-wrapper .stats .imdb > a').attr('href', (_i, oldHref) => oldHref.match(/.+(?=\/ratings)/)?.[0] ?? oldHref);
}
});
// swipe gestures prevent scrolling in title stats section (with external ratings, number of comments, etc.) on mobile layout because it's not set as excluded element
((fn) => document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', fn) : fn())(() => {
if (!unsafeWindow.jQuery) return;
const subDescs = Object.getOwnPropertyDescriptors(unsafeWindow.jQuery.fn.swipe),
desc = Object.getOwnPropertyDescriptor(unsafeWindow.jQuery.fn, 'swipe'),
oldValue = desc.value;
desc.value = function(...args) {
if (this.attr('id') === 'summary-wrapper') args[0].excludedElements = '#summary-ratings-wrapper .stats';
return oldValue.apply(this, args);
};
Object.defineProperty(unsafeWindow.jQuery.fn, 'swipe', desc);