Adds AniList, MyAnimeList, SIMKL, AnimeBytes, nzbs.moe, and SeaDex links to Sonarr anime series pages
Size
6.3 KB
Version
1.1.10
Created
Jan 17, 2026
Updated
18 days ago
1// ==UserScript==
2// @name Sonarr Anime Database Links
3// @description Adds AniList, MyAnimeList, SIMKL, AnimeBytes, nzbs.moe, and SeaDex links to Sonarr anime series pages
4// @version 1.1.10
5// @match http://192.168.1.50:8992/sonarr-anime/*
6// @match https://sonarr-anime.flowsflow.com/*
7// @icon http://192.168.1.50:8992/sonarr-anime/favicon.ico
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Sonarr Anime Database Links extension loaded');
13
14 let debounceTimer;
15 let lastProcessedTitle = '';
16
17 // Function to add anime database links in the toolbar
18 async function addAnimeLinks() {
19 // Check if we're on a series page
20 const titleElement = document.querySelector('.SeriesDetails-title-pJv1g');
21 if (!titleElement) {
22 console.log('Not on a series page');
23 return;
24 }
25
26 const seriesTitle = titleElement.textContent.trim();
27
28 // Check if we already processed this title
29 if (lastProcessedTitle === seriesTitle && document.getElementById('anime-db-container')) {
30 console.log('Anime database links already exist for this title');
31 return;
32 }
33
34 // Remove old container if it exists
35 if (document.getElementById('anime-db-container')) {
36 console.log('Removing old anime database links');
37 document.getElementById('anime-db-container')?.remove();
38 }
39
40 console.log('Series title:', seriesTitle);
41 lastProcessedTitle = seriesTitle;
42
43 // Find the toolbar
44 const toolbar = document.querySelector('.PageToolbarSection-left-E_sPJ');
45 if (!toolbar) {
46 console.log('Toolbar not found');
47 return;
48 }
49
50 // Get AniList ID for SeaDex and nzbs.moe
51 const anilistId = await getAniListId(seriesTitle);
52
53 // Create container for anime database links
54 const container = document.createElement('div');
55 container.id = 'anime-db-container';
56 container.style.cssText = 'display: flex; gap: 8px; align-items: center; margin-left: 12px;';
57
58 // Create buttons with compact style
59 const buttons = [
60 { name: 'AniList', url: `https://anilist.co/search/anime?search=${encodeURIComponent(seriesTitle)}`, color: '#02A9FF' },
61 { name: 'MAL', url: `https://myanimelist.net/anime.php?q=${encodeURIComponent(seriesTitle)}`, color: '#2E51A2' },
62 { name: 'SIMKL', url: `https://simkl.com/search/?type=anime&q=${encodeURIComponent(seriesTitle)}`, color: '#0B0F10' },
63 { name: 'AB', url: `https://animebytes.tv/torrents.php?searchstr=${encodeURIComponent(seriesTitle)}&action=advanced&search_type=title`, color: '#E74C3C' },
64 { name: 'NZBs', url: anilistId ? `https://nzbs.moe/series/al/${anilistId}` : 'https://nzbs.moe/', color: '#FF6B35' },
65 { name: 'SeaDex', url: anilistId ? `https://releases.moe/${anilistId}/` : 'https://releases.moe/about/', color: '#00D9FF' }
66 ];
67
68 buttons.forEach(btn => {
69 const button = document.createElement('button');
70 button.textContent = btn.name;
71 button.title = `Open on ${btn.name}`;
72 button.style.cssText = `
73 padding: 6px 12px;
74 background: ${btn.color};
75 color: white;
76 border: none;
77 border-radius: 4px;
78 cursor: pointer;
79 font-size: 13px;
80 font-weight: 500;
81 transition: opacity 0.2s;
82 `;
83
84 button.addEventListener('mouseenter', () => {
85 button.style.opacity = '0.8';
86 });
87
88 button.addEventListener('mouseleave', () => {
89 button.style.opacity = '1';
90 });
91
92 button.addEventListener('click', (e) => {
93 e.preventDefault();
94 window.open(btn.url, '_blank');
95 });
96
97 container.appendChild(button);
98 });
99
100 // Add separator before the container
101 const separator = document.createElement('div');
102 separator.className = 'PageToolbarSeparator-separator-N1WHF';
103
104 // Insert at the end of the toolbar
105 toolbar.appendChild(separator);
106 toolbar.appendChild(container);
107
108 console.log('Anime database links added successfully to toolbar');
109 }
110
111 // Function to get AniList ID from anime title
112 async function getAniListId(title) {
113 const query = `
114 query ($search: String) {
115 Media(search: $search, type: ANIME) {
116 id
117 }
118 }
119 `;
120
121 try {
122 const response = await fetch('https://graphql.anilist.co', {
123 method: 'POST',
124 headers: {
125 'Content-Type': 'application/json',
126 'Accept': 'application/json',
127 },
128 body: JSON.stringify({
129 query: query,
130 variables: { search: title }
131 })
132 });
133
134 const data = await response.json();
135 return data.data?.Media?.id || null;
136 } catch (error) {
137 console.error('Error fetching AniList ID:', error);
138 return null;
139 }
140 }
141
142 // Debounced version of addAnimeLinks
143 function debouncedAddAnimeLinks() {
144 clearTimeout(debounceTimer);
145 debounceTimer = setTimeout(() => {
146 addAnimeLinks();
147 }, 300);
148 }
149
150 // Function to initialize
151 function init() {
152 console.log('Initializing anime database links extension');
153
154 // Try to add the links immediately
155 addAnimeLinks();
156
157 // Watch for navigation changes (Sonarr is a SPA)
158 const observer = new MutationObserver(function() {
159 debouncedAddAnimeLinks();
160 });
161
162 // Observe the main content area for changes
163 const contentArea = document.querySelector('.PageContent-content-C67P9') || document.body;
164 observer.observe(contentArea, {
165 childList: true,
166 subtree: true
167 });
168
169 console.log('Observer set up for dynamic content');
170 }
171
172 // Wait for the page to be ready
173 if (document.readyState === 'loading') {
174 document.addEventListener('DOMContentLoaded', init);
175 } else {
176 init();
177 }
178})();