您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "Export to iCal" button to to YouTube Livestreams and Premieres which creates Calendar-compatible .ics files.
// ==UserScript== // @name YouTube: iCal Calendar Export for Livestreams and Premieres // @namespace org.sidneys.userscripts // @homepage https://gist.githubusercontent.com/sidneys/293fe8e9c3afdf50fe1db5be9346ac5a/raw/ // @version 0.7.4 // @description Adds a "Export to iCal" button to to YouTube Livestreams and Premieres which creates Calendar-compatible .ics files. // @author sidneys // @icon https://www.youtube.com/favicon.ico // @noframes // @match http*://www.youtube.com/* // @require https://gf.qytechs.cn/scripts/38888-greasemonkey-color-log/code/Greasemonkey%20%7C%20Color%20Log.js // @require https://gf.qytechs.cn/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js // @require https://cdn.jsdelivr.net/npm/[email protected]/moment.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/src/FileSaver.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/uuidv4.min.js // @require https://gitcdn.link/cdn/jamesbrond/ics.js/0b27e3cca5670758b63e880de9e49207d1f12290/ics.js // @run-at document-start // @grant unsafeWindow // ==/UserScript== /** * ESLint * @global */ /* global Debug, onElementReady, uuidv4, ics */ Debug = false /** * API Credentials * @default * @constant */ const apiKey = 'AIzaSyAxkkQLcQcshBDog7ev3jvjZmsjdDycgsQ' /** * API URL * @constant */ const apiBaseUrl = 'https://youtube.googleapis.com/youtube/v3' const apiEndpoint = '/videos' const apiBaseQuery = `part=snippet,liveStreamingDetails&key=${apiKey}` /** * Applicable URL paths * @default * @constant */ const urlPathList = [ '/channel', '/watch' ] /** * Local Filename of iCalendar entry * @constant */ const fileNameBase = 'youtube-calendar-event-' const fileExtension = 'ics' /** * Create iCal Calendar Event * @param {String} subject - Subject/Title * @param {String} description - Description * @param {String} location - Location * @param {String} begin - Beginning date * @param {String} end - Ending date * @param {Object=} rrule - Recurrence rule * @param {String=} filename - Local iCalendar File Name * @param {String=} extension - Local iCalendar File Extension */ let createCalendarEvent = (subject, description, location, begin, end, filename, extension = fileExtension) => { console.debug('createCalendarEvent') // Create iCal entry const icalEntry = new ics(uuidv4(), 'Calendar') // Add calendar event // icalEntry.addEvent(subject, description, location, begin, end, rrule, url) icalEntry.addEvent(subject, description, location, false, begin, end) // DEBUG console.debug('New iCalendar entry:') console.debug('filename:', filename) console.debug('extension:', extension) console.debug('subject:', subject) console.debug('location:', location) console.debug('begin:', (new Date(begin)).toString()) console.debug('end:', (new Date(end)).toString()) console.debug('description:', `${description.substring(0, 50)}…`) // Download .ics file icalEntry.download(filename, extension) } /** * On Button Click */ let onClickButton = () => { console.debug('onClickButton') // Lookup YouTube video Id const videoId = document.querySelector('ytd-watch-flexy').getAttribute('video-id') // Construct API URL for request const apiUrl = `${apiBaseUrl}${apiEndpoint}?${apiBaseQuery}&id=${videoId}` // DEBUG console.debug('videoId', videoId) console.debug('apiUrl', apiUrl) fetch(apiUrl) .then(res => res.json()) .then(data => { console.debug('data', data) const json = data const snippet = json?.items[0].snippet const liveStreamingDetails = json?.items[0].liveStreamingDetails if (!snippet) { console.error('API Error', 'Video:', 'snippet not found.') return } if (!liveStreamingDetails) { console.error('API Error', 'Video:', 'liveStreamingDetails not found.') return } // Calculate start & end time const startTimestamp = liveStreamingDetails.actualStartTime || liveStreamingDetails.scheduledStartTime const startDate = new Date(startTimestamp) const defaultEndDate = new Date(startDate.setSeconds(startDate.getSeconds() + 1800)) const defaultEndTimestamp = defaultEndDate.toISOString() const endTimestamp = liveStreamingDetails.actualEndTime || defaultEndTimestamp // Format metadata const subject = snippet.title.trim() const description = snippet.description.trim() const location = snippet.channelTitle.trim() const begin = startTimestamp const end = endTimestamp // Add custom metadata const url = `https://youtu.be/${videoId}` const urlAndDescription = `Link:\n${url}\n\n${description}` const filename = `${fileNameBase}${videoId}` // Create calendar event // createCalendarEvent(subject, description, location, begin, end, null, url, filename) createCalendarEvent(subject, urlAndDescription, location, begin, end, filename) }) } /** * Render Button 'Add to Playlist' * @param {Element} element - Target Element */ let renderButton = (element) => { console.debug('renderButton') // Create button element const buttonElement = document.createElement('div') buttonElement.innerHTML = ` <button class="ytp-offline-slate-button ytp-button"> <div class="ytp-offline-slate-button-icon"> <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#fff"><path d="M0 0h24v24H0z" fill="none"/> <path d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"/> </svg> </div> <div class="ytp-offline-slate-button-text">Export to iCal Calendar (.ics)</div> </button> ` // Add button element element.after(buttonElement) // Handle button click buttonElement.onclick = onClickButton // Status console.debug('rendered button') } /** * Init */ let init = () => { console.info('init') // Verify URL path if (!urlPathList.some(urlPath => window.location.pathname.startsWith(urlPath))) { return } // Wair for container onElementReady('.ytp-offline-slate-buttons', false, (element) => { // Render button renderButton(element) }) } /** * Handle in-page navigation (modern YouTube) * @listens window:Event#yt-navigate-finish */ window.addEventListener('yt-navigate-finish', () => { console.debug('window#yt-navigate-finish') init() })
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址