您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sets default sort and filter options for Radarr/Sonarr/...'s interactive search
- // ==UserScript==
- // @name *arr default sort & filter
- // @namespace https://github.com/Millio345/arr-default-sort-and-filter
- /* Since *arr's installation url is different for every user we need to match all links; Script checks for a 'Radarr' metadata tag before actually doing anything */
- // @include http://*
- // @include https://*
- // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @version 1.2
- // @author Millio
- // @description Sets default sort and filter options for Radarr/Sonarr/...'s interactive search
- // @license MIT
- // ==/UserScript==
- /* This script allows you to define default sorting and filter for your interactive searches for the arr websites.
- As of version 1.1 you no longer need to make any changes to this file.
- USAGE:
- After installing the extension, visit your Radarr/Sonarr/... installation and click the 'Configure *arr default sort & filter' button which should show up in your Userscript extension's menu on your browser
- Problems? Suggestions? Improvements? GitHub at https://github.com/Millio345/arr-default-sort-and-filter
- CHANGELOG:
- 1.2 - Add support for Lidarr, Sonarr, Readarr & Whisparr
- 1.1 - Move all configuration to UI to prevent users having to override this file with every update. Allow sorting by icon columns by entering the label text you see when moving your mouse over the element (span title)
- 1.0 - Initial release, basic sort & filter functionality
- */
- (function() {
- 'use strict';
- /* FUNCTIONS */
- // Sort the table by sortColumn and click "Filter" button if filterName is set
- function clickColumnAndFilter(tableDiv) {
- if(!(sortColumn === null || sortColumn === ""))
- {
- // Find all table headers
- let thElements = tableDiv.getElementsByTagName('th');
- let correctHeaderFound = false
- let headerList = [];
- // Loop through all table headers
- for (let i = 0; i < thElements.length; i++) {
- // Check if this is the table header we want to sort by
- if (thElements[i].textContent.trim().toLowerCase() === sortColumn.toLowerCase()) {
- correctHeaderFound = true;
- }
- // Not it, let's check if it has a span with the correct title for icon columns
- else if(thElements[i].querySelector('span') && thElements[i].querySelector('span').getAttribute('title').toLowerCase() === sortColumn.toLowerCase()) {
- correctHeaderFound = true;
- }
- else {
- if(thElements[i].textContent!==""){
- headerList.push(thElements[i].textContent);
- }
- else if(thElements[i].querySelector('span')){
- headerList.push(thElements[i].querySelector('span').getAttribute('title'));
- }
- }
- if(correctHeaderFound){
- // Click the <th> element
- thElements[i].click();
- // If we don't want ascending order, click again
- if(!sortAscending)
- thElements[i].click();
- break; // Stop the loop since we found the target column
- }
- }
- if(!correctHeaderFound){
- console.warn("Sort column '"+sortColumn+"' not found in table headers.")
- console.log("Valid columns: "+headerList.join(','))
- }
- }
- //If no filterName's set, or we've already clicked the filter button for this page load for this movie, this function is done. Otherwise, continue to clicking the filter button
- if(filterName === null || filterName === "" || filterButtonClicked)
- return
- // "Filter" button is first child of tableDiv parent
- let divs = tableDiv.parentElement.getElementsByTagName('div')
- for(let i =0; i < divs.length;i++){
- if(divs[i].className.startsWith('FilterMenu-filterMenu-'))
- {
- let filter = divs[i]
- let button = filter.querySelector('button')
- button.click()
- return;
- }
- }
- console.warn("Filter button not found. Unable to apply filter.");
- }
- // Click the filterName as soon as the filter menu is opened
- function clickChosenFilter(filterListNode) {
- // First make sure filterName is set
- if(filterName === null || filterName === ""){
- return
- }
- // Only click filter button once after opening a modal; Otherwise the user would not be able to override the filter since the script would always take over
- if(filterButtonClicked){
- return
- }
- // Get all filters
- let filters = filterListNode.getElementsByTagName('button')
- // Create a filterList for debug logging in case filter is not found
- let filterList = [];
- for(let i = 0; i < filters.length; i++) {
- let div = filters[i].querySelector('div');
- if(div !== null){
- if(div.textContent.trim()===filterName){
- // Got the right filter, click it
- filters[i].click();
- // Set filterButtonClicked to true
- filterButtonClicked = true
- return;
- }
- else
- filterList.push(div.textContent.trim());
- }
- }
- console.warn("Filter '"+filterName+"' not found in filter list.")
- console.log("Valid filters:"+filterList.join(','));
- }
- // Observe the body element so we know when modal is opened / closed
- function bodyClassChanged(mutationsList, _observer){
- mutationsList.forEach(mutation => {
- if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
- // Modal was closed; Set filterButtonClicked to false so we can reclick it next time the modal is opened
- if(!mutation.target.className.includes("Modal-modalOpen")) {
- filterButtonClicked = false;
- }
- }
- });
- }
- // This function is called by the mutationObserver when additional page content is added. Check if it contains the search table or the filter 'scroller'
- function handleMutations(mutationsList, _observer) {
- for (let mutation of mutationsList) {
- if (mutation.type === 'childList') {
- // Check if the added nodes include the target element
- if (mutation.addedNodes) {
- for (let node of mutation.addedNodes) {
- // Check if the added node is an element (type 1) which has classes
- if (node.nodeType === 1 && typeof node.className === 'string')
- {
- // Was a tableContainer in a modal added?
- if(node.className.includes('Table-tableContainer-')&&node.closest("[class^='ModalBody']")) {
- clickColumnAndFilter(node);
- return;
- }
- // Was a menu scroller added (This is the opened filter list)? Click the chosen filter
- else if(node.className.includes('MenuContent-scroller-')) {
- clickChosenFilter(node)
- }
- }
- }
- }
- }
- }
- }
- // Check current page url to see if we're on page with interactive search button or not
- function onPageWithInteractiveSearchButton(arr_type) {
- let pattern;
- // Depending on which arr site we're on we check the url
- if(arr_type==="Lidarr") {
- // artist/albums links end in /(artist|album)/artist-or-album-name
- pattern = /\/(artist|album)\/[\w-]+$/;
- }
- else if(arr_type==="Radarr") {
- // movie links end in /movie/NUMBER
- pattern = /\/movie\/\d+$/;
- }
- else if(arr_type==="Readarr") {
- // author links end in /author/NUMBER
- // Known limitation: The book page does not offer an interactive search modal and is currently not supported; Go to the author page and search from there.
- pattern = /\/author\/\d+$/;
- }
- else if(arr_type==="Sonarr") {
- // series links end in /series/series-name
- pattern = /\/series\/[\w-]+$/;
- }
- else if(arr_type==="Whisparr") {
- // site links end in /site/sitename
- pattern = /\/site\/[\w-]+$/;
- }
- // Return true / false depending on whether we're on a media page
- return pattern.test(window.location.href);
- }
- // When a movie page is loaded, start mutation observers and set filter clicked to false
- function interactiveSearchPageLoaded(){
- filterButtonClicked = false
- observer.observe(document.documentElement, { childList: true, subtree: true });
- body_observer.observe(document.querySelector('body'), { attributes: true, attributeFilter: ['class'], attributeOldValue: true });
- }
- // To prevent unintended consequences on other tables/buttons, stop observer when another page is loaded
- function nonInteractiveSearchPageLoaded(){
- observer.disconnect()
- body_observer.disconnect()
- }
- function loadScript(arr_type)
- {
- // Set Variable prefix to the arr type (f.e. SonarrInteractiveSearchFilterName
- let prefix = arr_type;
- // To prevent breaking changes to previous installations when this script only supported Radarr, Radarr variables don't have a prefix.
- if(prefix==="Radarr"){
- prefix="";
- }
- // Setup GM Config, which provides a nice UI to set the options
- gmc = new GM_config(
- {
- // We need unique id for each site, otherwise all non-specified values will be cleared when saving.
- 'id': prefix+'ArrDefaultSortAndFilter',
- "css": `#`+prefix+`ArrDefaultSortAndFilter {background: #2a2a2a;}
- #`+prefix+`ArrDefaultSortAndFilter .field_label {color: #fff;}
- #`+prefix+`ArrDefaultSortAndFilter .config_header {color: #fff; padding-bottom: 10px;}
- #`+prefix+`ArrDefaultSortAndFilter .reset {color: #f00; text-align: center;}
- #`+prefix+`ArrDefaultSortAndFilter .section_header {color: #fff; text-align: left; margin-top: 15px;margin-bottom: 5px; padding:5px;}
- #`+prefix+`ArrDefaultSortAndFilter .section_desc {background: #2a2a2a; color: #fff; text-align: left; border:none;}
- #`+prefix+`ArrDefaultSortAndFilter .config_var {text-align: left;}
- #`+prefix+`ArrDefaultSortAndFilter input { display: block; margin-top:5px; margin-bottom: 20px}
- #`+prefix+`ArrDefaultSortAndFilter .reset_holder {display: none;}`,
- 'title': arr_type+' Default Sort and Filter', // Panel Title
- 'fields': // Fields object
- {
- [prefix+'InteractiveSearchFilterName']: // This is the id of the field
- {
- 'label': 'Name of default filter to apply (leave empty to disable)', // Appears next to field
- 'section': [GM_config.create('Automatic Filter'), 'Automatically applies a filter when loading interactive search table'],
- 'type': 'text', // Makes this setting a text field
- 'title': 'Enter the name of the filter exactly as you see it in '+arr_type,
- 'default': '' // Default value if user doesn't change it
- },
- [prefix+'InteractiveSearchSortColumn']: // This is the id of the field
- {
- 'label': 'Default interactive search column to sort by (leave empty to disable)', // Appears next to field
- 'section': [GM_config.create('Automatic Sort'), 'If you want the script to automatically apply a filter when loading interactive search, enter the exact name of the filter here'],
- 'title': 'Column title exactly as you see it. For icons enter the value that appears when you move your mouse over the icon.',
- 'type': 'text', // Makes this setting a text field
- 'default': '' // Default value if user doesn't change it
- },
- [prefix+'InteractiveSearchSortAscending']: // This is the id of the field
- {
- 'label': 'Sort Ascending? (Check to sort A-Z / 0 - 9)', // Appears next to field
- 'type': 'checkbox', // Makes this setting a text field
- 'default': false // Default value if user doesn't change it
- },
- },
- 'events': {
- 'init': () => {
- // initialization complete, load values
- filterName = gmc.get([prefix+'InteractiveSearchFilterName']);
- sortColumn = gmc.get([prefix+'InteractiveSearchSortColumn']);
- sortAscending = gmc.get([prefix+'InteractiveSearchSortAscending']);
- },
- 'save': function () { // runs after values are saved
- // settings may have been changed, reload values
- filterName = gmc.get([prefix+'InteractiveSearchFilterName']);
- sortColumn = gmc.get([prefix+'InteractiveSearchSortColumn']);
- sortAscending = gmc.get([prefix+'InteractiveSearchSortAscending']);
- }
- }
- });
- // Add configuration menu item to extension
- GM_registerMenuCommand("Configure "+arr_type+" default sort & filter", show_config, "c");
- // Initial page load - Are we on a movie page?
- if(onPageWithInteractiveSearchButton(arr_type))
- interactiveSearchPageLoaded()
- // Code below to detect page change without reload *arr uses - https://stackoverflow.com/questions/6390341/how-to-detect-if-url-has-changed-after-hash-in-javascript
- let oldPushState = history.pushState;
- history.pushState = function pushState() {
- let ret = oldPushState.apply(this, arguments);
- window.dispatchEvent(new Event('pushstate'));
- window.dispatchEvent(new Event('locationchange'));
- return ret;
- };
- let oldReplaceState = history.replaceState;
- history.replaceState = function replaceState() {
- let ret = oldReplaceState.apply(this, arguments);
- window.dispatchEvent(new Event('replacestate'));
- window.dispatchEvent(new Event('locationchange'));
- return ret;
- };
- window.addEventListener('popstate', () => {
- window.dispatchEvent(new Event('locationchange'));
- });
- // locationchange event is fired when url is changed thanks to code above
- window.addEventListener('locationchange', function () {
- //Are we on a movie page? Call appropriate function
- if(onPageWithInteractiveSearchButton(arr_type))
- interactiveSearchPageLoaded()
- // When on another page, disable the mutation observer
- else
- nonInteractiveSearchPageLoaded()
- });
- }
- function show_config()
- {
- gmc.open();
- }
- /* END FUNCTIONS */
- // Following code is ran on every single tab so should be kept as light as possible
- // Support all sites listed under Media Automation on https://wiki.servarr.com/
- let supportedSites = ["Lidarr","Radarr","Readarr","Sonarr","Whisparr"];
- // Check if we're on a supported site (site with metadata description 'Radarr'/'Sonarr'/...)
- let description = document.querySelector('meta[name="description"]');
- if(description && supportedSites.includes(description.content)){
- let arr_type=description.content
- console.log(arr_type+" installation found. Watching for interactive search table.")
- // Define some variables for global use through the script
- // Create a new MutationObserver since *arr sites dynamically add elements after pageload
- var observer = new MutationObserver(handleMutations);
- // Observe the body element for changes to the class (= modal opened / closed)
- var body_observer = new MutationObserver(bodyClassChanged);
- // *arr sites uses ajax to change pages; We only want to click the filter button once per movie (otherwise the filter button is completely blocked from user interaction)
- var filterButtonClicked = false;
- // Variable to store gm config
- var gmc;
- // filter and sort options. Variables automatically get populated by the GMC config menu on init
- var filterName;
- var sortColumn;
- var sortAscending;
- // Load the script functionality
- loadScript(arr_type);
- }
- })();