nicovideo downloader

script for downloading videos from the nicovideo website

// ==UserScript==
// @name         nicovideo downloader
// @description  script for downloading videos from the nicovideo website
// @namespace    kwlNjR37xBCMkr76P5eKA88apmOClCfZ
// @author       kwlNjR37xBCMkr76P5eKA88apmOClCfZ
// @version      0002
// @match        *://www.nicovideo.jp/watch/sm*
// @match        *://ext.nicovideo.jp/?sm*
// @website      https://greasyfork.org/en/scripts/548403
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzI1MjUyNSIgZD0iTTIwLjM4NC4wMjJIMy42MTZBMy41OTUgMy41OTUgMCAwIDAgLjAyMiAzLjYxNnYxNi43NjhhMy41OTQgMy41OTQgMCAwIDAgMy41OTQgMy41OTRoMTYuNzY4YTMuNTkzIDMuNTkzIDAgMCAwIDMuNTk0LTMuNTk0VjMuNjE2QTMuNTk0IDMuNTk0IDAgMCAwIDIwLjM4NC4wMjIiLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNMjAuMjEzIDUuNzI0aC01LjgxNmwyLjM4OC0yLjI3NWEuODUuODUgMCAwIDAgLjA0MS0xLjE4Mi44LjggMCAwIDAtMS4xNTMtLjA0M0wxMiA1LjcyNGwtMy42NzMtMy41YS44LjggMCAwIDAtMS4xNTMuMDQzLjg1Ljg1IDAgMCAwIC4wNDIgMS4xODJsMi4zODcgMi4yNzVIMy43ODhBMS44IDEuOCAwIDAgMCAyIDcuNTM1djEwLjg2M2MwIDEgLjgwMiAxLjgxMiAxLjc4OCAxLjgxMmgyLjI2NmwxLjM1IDEuNTlhLjUxNy41MTcgMCAwIDAgLjgxNiAwbDEuMzUtMS41OWg0Ljg2bDEuMzUgMS41OWEuNTE3LjUxNyAwIDAgMCAuODE2IDBsMS4zNS0xLjU5aDIuMjY2Yy45OSAwIDEuNzg4LS44MTEgMS43ODgtMS44MTJWNy41MzVjMC0xLS43OTktMS44MS0xLjc4Ny0xLjgxIi8+PC9zdmc+
// @grant        GM.setValue
// @grant        GM.getValue
// @license      CC0 1.0
// ==/UserScript==

(function() {
	'use strict';

	let targetContainer;
	let observer;
	const SCRIPT_SRC_720p = "https://www.nicozon.net/js/bookmarklet_old_720p.js";
	const SCRIPT_SRC_480p = "https://www.nicozon.net/js/bookmarklet_old_480p.js";
	const SCRIPT_SRC_360p = "https://www.nicozon.net/js/bookmarklet_old_360p.js";
	const SCRIPT_SRC_audio = "https://www.nicozon.net/js/bookmarklet_old_audio.js";

	// Log script initialization
	console.log('NicoVideo Downloader script initialized');

	// Function to extract video ID from URL
	function getVideoId() {
		try {
			const url = window.location.href;
			const match = url.match(/sm(\d+)/);
			const videoId = match ? match[0] : null;

			if (!videoId) {
				console.error('Could not extract video ID from URL:', url);
			} else {
				console.log('Extracted video ID:', videoId);
			}

			return videoId;
		} catch (error) {
			console.error('Error extracting video ID:', error);
			return null;
		}
	}

	// Function to create download links
	function createDownloadLinks() {
		try {
			const videoId = getVideoId();
			if (!videoId) {
				console.error('Cannot create download links without video ID');
				return null;
			}

			// Create container for links
			const linksContainer = document.createElement('div');
			linksContainer.className = 'download-links';
			linksContainer.style.cssText = `
				display: inline-block;
				margin-left: 2px;
			`;

			const svgContainer = document.createElement('span');
			svgContainer.style.cssText = `
				display: inline-flex;
				align-items: center;
				justify-content: center;
			`;
			svgContainer.title = "Download";
			svgContainer.innerHTML = `
				<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w_font h_font icon icon-tabler icons-tabler-outline icon-tabler-download">
					<path stroke="none" d="M-2.016-2.016h28.038v28.038H-2.016V-2.016Z" fill="none"/>
					<path d="M2.657,17.844v2.336c0,1.29,1.046,2.336,2.336,2.336h14.019c1.29,0,2.336-1.046,2.336-2.336v-2.336"/>
					<path d="M6.162,10.835l5.841,5.841,5.841-5.841"/>
					<path d="M12.003,2.657v14.019"/>
				</svg>
			`;
			linksContainer.appendChild(svgContainer);

			const label = document.createElement('span');
			label.textContent = ':';
			label.style.marginLeft = '3px';
			label.style.marginRight = '6px';
			linksContainer.appendChild(label);

			// Create download options
			const options = [
				{ text: '720p', title: 'Download video (720p)', quality: '720p', script: SCRIPT_SRC_720p },
				{ text: '480p', title: 'Download video (480p)', quality: '480p', script: SCRIPT_SRC_480p },
				{ text: '360p', title: 'Download video (360p)', quality: '360p', script: SCRIPT_SRC_360p },
				{ text: '♫', title: 'Download audio', quality: 'audio', script: SCRIPT_SRC_audio }
			];

			options.forEach((option, index) => {
				const link = document.createElement('a');
				link.href = `https://ext.nicovideo.jp/?${videoId}`;
				link.textContent = option.text;
				link.title = option.title;
				link.dataset.quality = option.quality;
				link.dataset.script = option.script;
				link.style.cssText = `
					color: #ff9900;
					text-decoration: none;
					font-weight: bold;
					cursor: pointer;
				`;
				link.addEventListener('mouseover', function() {
					this.style.textDecoration = 'underline';
				});
				link.addEventListener('mouseout', function() {
					this.style.textDecoration = 'none';
				});
				link.addEventListener('click', async function(e) {
					e.preventDefault();
					try {
						// Store the selected quality using GM.setValue
						await GM.setValue('nicovideoDownloadQuality', this.dataset.quality);
						await GM.setValue('nicovideoDownloadScript', this.dataset.script);
						console.log('Stored download preference:', this.dataset.quality);
						window.open(this.href, '_blank');
					} catch (error) {
						console.error('Error storing download preference:', error);
						alert('Error saving download preference. Please check console for details.');
					}
				});

				linksContainer.appendChild(link);

				// Add separator except for the last item
				if (index < options.length - 1) {
					const separator = document.createElement('span');
					separator.textContent = '|';
					separator.style.cssText = 'color: #ccc; margin: 0 4px;';
					linksContainer.appendChild(separator);
				}
			});

			console.log('Download links created successfully');
			return linksContainer;
		} catch (error) {
			console.error('Error creating download links:', error);
			return null;
		}
	}

	function addDownloadLinks() {
		try {
			targetContainer = document.querySelector('.d_flex.gap_x2.text-layer_lowEm.fs_s.fw_bold.white-space_nowrap.ai_center');
			if (targetContainer) {
				// Check if links already exist to avoid duplicates
				if (!targetContainer.querySelector('.download-links')) {
					const linksContainer = createDownloadLinks();
					if (linksContainer) {
						targetContainer.appendChild(linksContainer);
						console.log('Download links added to page');

						// Stop observing once we've successfully added the links
						if (observer) {
							observer.disconnect();
							console.log('MutationObserver disconnected');
						}
					}
				} else {
					console.log('Download links already exist, skipping');
				}
			} else {
				console.log('Target container not found yet');
				return false;
			}
			return true;
		} catch (error) {
			console.error('Error adding download links:', error);
			return false;
		}
	}

	// Function to handle different page types
	async function handlePage() {
		try {
			console.log('Handling page:', window.location.hostname);
			const videoId = getVideoId();
			if (!videoId) {
				console.error('No video ID found, stopping execution');
				return;
			}

			if (window.location.hostname === 'www.nicovideo.jp') {
				console.log('On main nicovideo page');
				// Try immediately first
				const success = addDownloadLinks();

				// If not found, set up MutationObserver to watch for DOM changes
				if (!success) {
					console.log('Setting up MutationObserver to watch for target container');
					observer = new MutationObserver(function(mutations) {
						console.log('DOM mutation observed', mutations.length, 'changes');
						mutations.forEach(function(mutation) {
							if (mutation.addedNodes && mutation.addedNodes.length > 0) {
								addDownloadLinks();
							}
						});
					});

					// Start observing the document body for added nodes
					observer.observe(document.body, {
						childList: true,
						subtree: true
					});

					// Set a timeout as fallback
					/*setTimeout(() => {
						if (observer) {
							observer.disconnect();
							console.log('MutationObserver disconnected after timeout');
						}
						const finalAttempt = addDownloadLinks();
						if (!finalAttempt) {
							console.error('Failed to add download links after timeout');
						}
					}, 30000); // 30 seconds as fallback*/
				}
			}
			else if (window.location.hostname === 'ext.nicovideo.jp') {
				console.log('On ext.nicovideo page');
				// On ext.nicovideo page, check if we have a stored quality preference
				const quality = await GM.getValue('nicovideoDownloadQuality');
				const scriptSrc = await GM.getValue('nicovideoDownloadScript');

				console.log('Retrieved stored values - Quality:', quality, '; Script:', scriptSrc);

				const p = document.createElement("p");
				p.textContent = "Download: " + (quality || 'No quality selected');
				document.body.appendChild(p);

				// Clean up stored values
				await GM.setValue('nicovideoDownloadQuality', '');
				await GM.setValue('nicovideoDownloadScript', '');
				console.log('Cleared stored values');

				if (scriptSrc) {
					console.log('Loading download script:', scriptSrc);
					try {
						const script = document.createElement('script');
						script.setAttribute('charset', 'utf-8');
						script.src = scriptSrc;
						script.onload = function() {
							console.log('Download script loaded successfully');
						};
						script.onerror = function() {
							console.error('Failed to load download script:', scriptSrc);
							alert('Failed to load download script. Please try again.');
						};
						document.body.appendChild(script);
					} catch (error) {
						console.error('Error loading download script:', error);
					}
				} else {
					console.log('No script source found, creating download links instead');
					const linksContainer = createDownloadLinks();
					if (linksContainer) {
						document.body.appendChild(linksContainer);
					}
				}
			} else {
				console.warn('Unsupported hostname:', window.location.hostname);
			}
		} catch (error) {
			console.error('Error in handlePage function:', error);
		}
	}

	// Run when DOM is ready
	if (document.readyState === 'loading') {
		console.log('DOM not ready, adding DOMContentLoaded listener');
		document.addEventListener('DOMContentLoaded', function() {
			console.log('DOMContentLoaded fired');
			handlePage();
		});
	} else {
		console.log('DOM already ready, executing immediately');
		handlePage();
	}
})();

// https://www.youtube.com/watch?v=sztJ8cO4mak
// https://www.nicozon.net/