// ==UserScript==
// @name DTF Save Bookmarks
// @namespace http://tampermonkey.net/
// @version 2024-03-26
// @description Быстро скачать закладки
// @author Demon59901
// @match https://dtf.ru/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=dtf.ru
// @grant none
// ==/UserScript==
// НАСТРОЙКИ
const THEME = 'auto' // auto, dark, light
const SAVE_COMMENTS = true
const COMMENT_LIMIT = 10000
const OFFLINE_STYLES = true
const OFFLINE_IMAGES = true
const BOOKMARKS_HTML = false
const USE_DATE_PREFIX = true
const LINK_TO_ARCHIVE = true
const PARALLEL_DOWNLOADS = true
const MAX_PARALLEL_DOWNLOADS = 2 // <= 5
// Время на загрузку DOM
const DELAY = 1000
// ========
let prevToken = ''
const imageCache = new Map()
const downloadStartEvent = new Event('downloadStart')
const downloadStopEvent = new Event('downloadStop')
function setup() {
const url = window.location.href
const profileMatch = url.match(/dtf\.ru\/u\/(\d+)(?:-[\w-]+)?\/?$/)
const bookmarksMatch = url.match(/dtf\.ru\/bookmarks(?:\/posts)?\/?$/)
if (!profileMatch && !bookmarksMatch) return
const placeForButton = document.querySelector('.bookmarks__filter, .subsite-feed-sorting')
if (!placeForButton || placeForButton.querySelector('#download-button')) return
placeForButton.style.display = 'flex'
placeForButton.style.alignItems = 'baseline'
placeForButton.style.justifyContent = 'space-between'
const downloadButton = document.createElement('button')
const buttonText = `Скачать ${bookmarksMatch ? 'закладки' : 'посты'}`
downloadButton.id = 'download-button'
downloadButton.innerText = buttonText
downloadButton.style.fontSize = '15px'
downloadButton.style.cursor = 'pointer'
downloadButton.style.color = 'color: var(--theme-color-text-primary)'
downloadButton.className = 'sidebar-item-inline'
downloadButton.onclick = () => bookmarksMatch ? downloadBookmarks() : downloadPosts()
downloadButton.addEventListener('downloadStart', e => {
e.target.innerText = 'Загрузка...'
e.target.disabled = true
e.target.style.opacity = '0.5'
})
downloadButton.addEventListener('downloadStop', e => {
e.target.innerText = buttonText
e.target.disabled = false
e.target.style.removeProperty('opacity')
})
placeForButton.appendChild(downloadButton)
async function downloadBookmarks() {
try {
const token = prompt('TOKEN', prevToken || 'Вставьте ваш токен авторизации')
if (!token) return
prevToken = token
const count = parseInt(prompt('COUNT', '10000'))
if (isNaN(count)) return
const offset = parseInt(prompt('OFFSET', '0'))
if (isNaN(offset)) return
downloadButton.dispatchEvent(downloadStartEvent)
const url = `https://api.dtf.ru/v2.5/bookmarks?type=posts&count=${count}&offset=${offset}`
const headers = { 'Jwtauthorization': token }
const result = (await (await fetchData(url, headers)).json()).result
const posts = result.map(({ data: { id, url, title, date }, date: saveDate }) => ({
id, url, title: title || url, date, prefixDate: saveDate
}))
await processPosts(posts)
} catch (error) {
alert(error)
} finally {
downloadButton.dispatchEvent(downloadStopEvent)
}
}
async function downloadPosts() {
try {
const count = parseInt(prompt('COUNT', '10000'))
if (isNaN(count)) return
const offset = parseInt(prompt('OFFSET', '0'))
if (isNaN(offset)) return
downloadButton.dispatchEvent(downloadStartEvent)
const userId = profileMatch[1]
const sorting = 'new'
let postItems = []
let end = null
do {
const url = `https://api.dtf.ru/v2.5/timeline?&sorting=${sorting}&subsitesIds=${userId}${end ? `&lastId=${end}` : ''}`
const { lastId, items } = (await (await fetchData(url)).json()).result
postItems.push(...items)
end = lastId
} while (end !== 0 && postItems.length < (offset + count))
postItems = postItems.splice(offset, count)
const posts = postItems.map(({ data: { id, title, date, subsite: { uri: subsiteUri } } }) => {
const baseUrl = new URL('https://dtf.ru')
const url = new URL(`${subsiteUri ? subsiteUri : `/u/${userId}`}/${id}`, baseUrl).href
return { url, id, title: title || url, date, prefixDate: date }
})
await processPosts(posts)
} catch (error) {
alert(error)
} finally {
downloadButton.dispatchEvent(downloadStopEvent)
}
}
async function processPosts(posts) {
if (!posts.length) {
alert('Ничего не найдено')
return
}
if (BOOKMARKS_HTML && confirm(`Сохранить найденные ссылки (${posts.length}) в bookmarks.html?`))
createBookmarksHtml(posts)
if (confirm(`Сохранить ${posts.length} постов в папку загрузок?`)) {
downloadButton.dispatchEvent(downloadStartEvent)
await (PARALLEL_DOWNLOADS ? downloadPagesInParallel(posts) : downloadPagesSequentially(posts))
downloadButton.dispatchEvent(downloadStopEvent)
console.log('✅ DONE')
}
}
}
async function downloadPage({ id, url, title, date, prefixDate }, index, total) {
try {
console.log(`⬇️ Downloading ${index + 1} of ${total}:`, { title, url })
const doc = new DOMParser().parseFromString(await (await fetchData(url)).text(), 'text/html')
const baseUrl = new URL(url)
doc.querySelectorAll('[href], [src]').forEach(el => {
el.href && (el.href = new URL(el.href, baseUrl).href)
el.src && (el.src = new URL(el.src, baseUrl).href)
})
const content = doc.querySelector('.content')
if (!content)
throw new Error('Блок с контентом не найден')
content.classList.remove('content--nsfw')
content.querySelectorAll('.block-wrapper__overlay, .content-nsfw, .content-footer__share, .content-footer__item--dislike') // .content__counters, .content-footer,
.forEach(el => el.remove())
const headerActions = content.querySelector('.content-header__actions')
headerActions.appendChild(createLinkButton(baseUrl.href, 'Ссылка на пост'))
const timestamp = 1670976000 // 14.12.2022 0:00:00
if (LINK_TO_ARCHIVE && date <= timestamp) {
try {
const {
archived_snapshots: { closest: { url: archivedUrl = '' } = {} }
} = await (await fetchData(`https://archive.org/wayback/available?url=${url}×tamp=${timestamp}`)).json()
archivedUrl && (headerActions.prepend(createLinkButton(archivedUrl, 'Ссылка на архив')))
} catch (error) {
console.error(`Ошибка при попытке получить ссылку на веб-архив (${url}):`, error)
}
}
content.querySelectorAll('time').forEach(time => time.innerText = time.title.slice(0, 10))
if (OFFLINE_IMAGES) {
await replaceImagesWithBase64(content)
content.querySelectorAll('picture').forEach(el => {
const img = el.querySelector('img')
img && img.src.startsWith('data:image') && el.querySelectorAll('source').forEach(source => source.remove())
})
} else {
content.querySelectorAll('[data-loaded=true]').forEach(el => el.dataset.loaded = 'false')
}
let mediaSrc = {}
const fallback = {
youtube: 'https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1&playsinline=1',
video: 'https://leonardo.osnova.io/0f80ee3e-bb93-5788-a578-336570c3fb2d/-/format/mp4#t=0.1'
}
try {
const {
author: { avatar: { data: { uuid: avatarId } } }, blocks
} = (await (await fetchData(`https://api.dtf.ru/v2.5/content?id=${id}`)).json()).result
await Promise.all(blocks.map(block => gatherMediaSrc(block, mediaSrc)))
content.querySelectorAll('.author__avatar video')
.forEach(avatar => avatar.src = `https://leonardo.osnova.io/${avatarId}`)
content.querySelectorAll('.content__blocks video')
.forEach((video, idx) => video.src = mediaSrc.gif[idx] || fallback.video)
content.querySelectorAll('.block-quiz').forEach((quiz, idx) => {
try {
const quizData = mediaSrc.quiz[idx]
quiz.querySelector('.block-quiz__options').classList.add('block-quiz__options--disabled')
quiz.querySelector('.block-quiz__footer span').innerText = `${Object.values(quizData)[0].total} голосов`
quiz.querySelectorAll('.block-quiz-option').forEach(option => {
const title = option.querySelector('.block-quiz-option__text').innerText
const optionData = quizData[title]
const control = option.querySelector('.block-quiz-option__control')
const progress = option.querySelector('.block-quiz-option__progress')
control.className = 'block-quiz-option__value'
control.innerText = `${optionData.percentage}%`
progress.classList.add('block-quiz-option__progress--minimal')
progress.style = `--percentage: ${optionData.percentage}%;`
})
} catch (error) {
console.error(`Ошибка при заполнении результатов опроса (${url}):`, error)
}
})
} catch (error) {
console.error(`Ошибка при загрузке данных (${url}):`, error)
}
if (!mediaSrc.video) {
content.querySelectorAll('.andropov-external-video__overlay').forEach(() => {
mediaSrc.video = mediaSrc.video || []
mediaSrc.video.push(fallback.youtube)
})
}
const linkToMainCss = getLinkToCss(/\/assets\/index-\w+?\.css$/i)
const linkToCommentsCss = getLinkToCss(/\/assets\/comments-\w+?\.css$/i)
const offlineMainStyles = OFFLINE_STYLES && await getCachedStyles(linkToMainCss, '--cached-main-css')
const offlineCommentsStyles = SAVE_COMMENTS && OFFLINE_STYLES && await getCachedStyles(linkToCommentsCss, '--cached-comments-css')
const commentsHtml = SAVE_COMMENTS && await getComments() || ''
const headHtml = `
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>${title}</title>
${offlineMainStyles ? `<style>${offlineMainStyles}</style>` : `<link rel="stylesheet" href="${linkToMainCss}">`}
${offlineCommentsStyles ? `<style>${offlineCommentsStyles}</style>` : `<link rel="stylesheet" href="${linkToCommentsCss}">`}
<style>
body {
height: auto;
padding: var(--layout-view-offset-y) 0;
word-break: break-word;
}
.content, .comments {
max-width: var(--layout-max-content-width);
margin: 0 auto;
}
.comments {
margin-top: var(--layout-island-gap-y);
}
</style>
`
const script = `
<script>
const videoSrc = ${JSON.stringify(mediaSrc.video)}
const imageSrc = ${JSON.stringify(mediaSrc.image)}
document.querySelectorAll('a[href]').forEach(link => link.target = '_blank')
document.querySelectorAll('.andropov-external-video__overlay').forEach((video, idx) => {
video.addEventListener('click', () => {
const player = document.createElement('iframe')
player.classList.add('andropov-external-video__player')
player.allow = 'autoplay;accelerometer;clipboard-write;encrypted-media;gyroscope;picture-in-picture'
player.allowFullscreen = true
player.src = videoSrc[idx]
video.parentElement.replaceChildren(player)
requestAnimationFrame(() => {
player !== null && (player.allow = player.allow.replace('autoplay;', ''))
})
})
})
document.querySelectorAll('.media--zoom, .gallery__item, .comment-media .andropov-image--zoom').forEach((img, idx) => {
img.querySelector('.gallery__more')?.remove()
img.addEventListener('click', () => {
window.open(imageSrc[idx], '_blank')
})
})
document.querySelectorAll('[data-loaded=false] img, video').forEach(el => {
const eventType = el.tagName === 'IMG' ? 'load' : 'loadeddata'
el.addEventListener(eventType, () => {
requestAnimationFrame(() => {
el.closest('[data-loaded]').dataset.loaded = 'true'
})
}, { once: true })
})
setTimeout(() => document.querySelectorAll('[data-loaded=false]').forEach(el => el.dataset.loaded = 'true'), 5000)
</script>
`
const icons = `
<svg id="__svg__icons__dom__" xmlns="http://www.w3.org/2000/svg" xmlns:link="http://www.w3.org/1999/xlink" style="position: absolute; width: 0px; height: 0px;"><symbol fill="none" viewBox="0 0 24 24" id="bookmark"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 7a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v12.045c0 1.628-1.875 2.539-3.155 1.534l-4.32-3.395a.85.85 0 0 0-1.05 0l-4.32 3.395C5.875 21.584 4 20.672 4 19.045V7Zm4-2a2 2 0 0 0-2 2v11.942l4.24-3.33a2.85 2.85 0 0 1 3.52 0l4.24 3.33V7a2 2 0 0 0-2-2H8Z" fill="currentColor"></path></symbol><symbol fill="none" viewBox="0 0 24 24" id="comment"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.979 11.736c0-5.4-4.383-8.736-9.244-8.736C6.883 3 2.518 6.323 2.5 11.703c-.018 5.38 4.269 8.768 9.236 8.768 1.306 0 2.353-.066 3.619-.57.191-.076.507-.082.81.02l3.044 1.013c.718.216 1.363-.124 1.723-.48.375-.372.707-1.018.511-1.742l-.007-.028-.998-2.992c-.1-.39-.1-.84-.017-1.07l.055-.155c.277-.768.503-1.393.503-2.731ZM11.735 5C7.754 5 4.5 7.653 4.5 11.736s3.254 6.735 7.236 6.735c1.22 0 1.966-.064 2.88-.428.73-.29 1.547-.232 2.184-.02l2.596.866-.875-2.622-.007-.029c-.168-.622-.246-1.535.024-2.29l.06-.168c.246-.682.38-1.054.38-2.044 0-4.082-3.26-6.736-7.243-6.736Z" fill="currentColor"></path></symbol><symbol fill="none" viewBox="0 0 24 24" id="like"><path d="M3.224 8.104C3.751 6.186 5.284 4.5 7.876 4.5c1.563 0 2.64.574 3.391 1 .305.173.558.377.733.5.175-.123.428-.327.733-.5.751-.426 1.828-1 3.39-1 2.592 0 4.126 1.686 4.653 3.604.519 1.887.045 4.248-1.276 5.896-1.218 1.518-2.8 2.809-4.212 3.786-.709.492-1.366.896-1.883 1.18-.419.23-.912.534-1.405.534-.5 0-.983-.302-1.405-.534a22.27 22.27 0 0 1-1.883-1.18C7.3 16.81 5.718 15.518 4.5 14c-1.321-1.648-1.795-4.009-1.276-5.896Z" stroke="currentColor" stroke-width="1.995" stroke-linecap="round" stroke-linejoin="round"></path></symbol><symbol viewBox="0 0 24 24" id="pin"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.166 4.235c.525-1.276 2.237-1.683 3.262-.653l3.99 4.012.003.002c1.02 1.019.628 2.736-.656 3.26-2.774 1.344-3.774 3.867-3.774 7.146 0 .91-.596 1.56-1.256 1.829-.653.265-1.53.22-2.16-.42L9.95 16.787 4.6 20.8a1 1 0 0 1-1.4-1.4l4.013-5.35-2.623-2.623-.002-.002c-.635-.629-.686-1.503-.42-2.16.269-.663.922-1.254 1.83-1.254 3.287 0 5.822-.998 7.168-3.776Zm1.847.768a1.011 1.011 0 0 1-.03.067c-1.8 3.756-5.255 4.936-8.96 4.941a.11.11 0 0 0-.005.016L9.24 13.25a1 1 0 0 1 .093 1.307L9 15.002l.446-.334a1 1 0 0 1 1.307.093l3.221 3.22a.12.12 0 0 0 .018-.007c.006-3.701 1.191-7.14 4.94-8.936.022-.01.044-.02.066-.028A.03.03 0 0 0 19 9.004L15.02 5a.037.037 0 0 0-.006.002Z"></path></symbol></svg>
`
const blob = new Blob([`<!DOCTYPE html><html data-theme="${THEME}" lang="ru"><head>${headHtml}</head><body>${content.outerHTML}${commentsHtml}${script}${icons}</body></html>`], { type: 'text/html' })
const downloadUrl = window.URL.createObjectURL(blob)
const a = document.createElement('a')
const prefix = USE_DATE_PREFIX && !isNaN(parseInt(prefixDate))
? `${new Date(prefixDate * 1000).toISOString().replace(/-/g, '.').slice(0, 10)} ` : ''
a.href = downloadUrl
a.download = `${prefix}${title}.html`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(downloadUrl)
a.remove()
function createLinkButton(href, text) {
const linkButton = document.createElement('a')
linkButton.href = href
linkButton.textContent = text
linkButton.classList.add('button', 'button--size-s', 'button--type-minimal')
return linkButton
}
async function replaceImagesWithBase64(element) {
const imgPromises = Array.from(element.querySelectorAll('img')).map(async img => {
try {
let base64Image = imageCache.get(img.src)
if (!base64Image) {
const blob = await (await fetchData(img.src)).blob()
base64Image = await blobToBase64(blob)
imageCache.set(img.src, base64Image)
}
img.src = base64Image
} catch (error) {
console.error(`Ошибка при загрузке изображения (${img.src}, ${url}):`, error)
imageCache.set(img.src, img.src)
}
})
await Promise.all(imgPromises)
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(blob)
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
})
}
async function gatherMediaSrc({ type, data }, srcMap) {
try {
switch (type) {
case 'media':
data.items.forEach(({ image: { data } }, _, arr) => imageDataHandler(srcMap, data, arr.length))
break
case 'video':
const { name, id } = data.video?.data.external_service || data.external_service
srcMap.video = srcMap.video || []
srcMap.video.push(getExternalSrc(name, id))
break
case 'osnovaEmbed':
data.osnovaEmbed.data.image?.data &&
imageDataHandler(srcMap, data.osnovaEmbed.data.image.data, 0)
break
case 'tweet':
data.tweet.data.tweet_data.extended_entities?.media?.forEach(img => {
srcMap.image = srcMap.image || []
srcMap.image.push(img.media_url)
})
break
case 'image' || 'movie':
imageDataHandler(srcMap, data, 1)
break
case 'quiz':
const { result: { items } } = await (await fetchData(`https://api.dtf.ru/v2.1/quiz/${data.hash}/results`)).json()
srcMap.quiz = srcMap.quiz || []
const quizResult = Object.keys(data.items).reduce((acc, key) => {
if (items.hasOwnProperty(key)) {
acc[data.items[key]] = items[key]
}
return acc
}, {})
srcMap.quiz.push(quizResult)
break
default:
break
}
} catch (error) {
console.error(`Ошибка при извлечении данных из объекта (${url}):\n${data}`, error)
}
}
function imageDataHandler(srcMap, { type, uuid, external_service: { mp4_url } }, count = 1) {
if ((type === 'gif' || type === 'mp4') && (count === 1 || count === 0)) {
const src = mp4_url || `https://leonardo.osnova.io/${uuid}/-/format/mp4#t=0.1`
srcMap.gif = srcMap.gif || []
srcMap.gif.push(src)
} else if (count !== 0) {
srcMap.image = srcMap.image || []
srcMap.image.push(`https://leonardo.osnova.io/${uuid}`)
}
}
function getExternalSrc(name, id) {
const n = window.location.hostname
switch (name) {
case 'youtube':
return `https://www.youtube.com/embed/${id}?autoplay=1&playsinline=1`
case 'coub':
return `https://coub.com/embed/${id}?autoplay=true`
case 'twitch-clip':
return `https://clips.twitch.tv/embed?clip=${id}&parent=${n}&autoplay=true`
default:
return
}
}
async function getComments() {
try {
const { result: { items: comments } } = await (await fetchData(`https://api.dtf.ru/v2.4/comments?sorting=hotness&contentId=${id}`)).json()
if (comments.length === 0) return ''
let commentsHtml = ''
comments.slice(0, COMMENT_LIMIT).forEach(comment => {
commentsHtml += insertComment(comment)
comment.media.forEach(media => gatherMediaSrc(media, mediaSrc))
})
const commentsEl = new DOMParser().parseFromString(commentsHtml, 'text/html')
if (OFFLINE_IMAGES) {
await replaceImagesWithBase64(commentsEl)
commentsEl.querySelectorAll('picture').forEach(el => {
const img = el.querySelector('img')
img && img.src.startsWith('data:image') && el.querySelectorAll('source').forEach(source => source.remove())
})
} else {
commentsEl.querySelectorAll('[data-loaded=true]').forEach(el => el.dataset.loaded = 'false')
}
const commentsLeft = comments.length - COMMENT_LIMIT
return `
<div class="comments${commentsLeft > 0 ? ' comments--limited' : ''}">
<div class="comments-header">
<div class="comments-header__title">${comments.length} комментариев</div>
</div>
<div class="comments-tree">
${commentsEl.body.innerHTML}
</div>
${commentsLeft <= 0 ? '' : `
<div class="comments-limit comments-limit--bottom">
<a href="${url}#comments" class="link-button link-button--default comments-limit__expand">
Ещё ${commentsLeft} в оригинальном посте
</a>
</div>
`}
</div>
`
function insertComment({ id, author, date, likes, media, level, text, isPinned }) {
const baseUrl = new URL('https://dtf.ru')
const authorUrl = new URL(`${author.uri || `/u/${author.id}`}`, baseUrl).href
const commentUrl = `${url}?comment=${id}`
const time = new Date(date * 1000)
const linkRegex = /(?:https?:\/\/)?(?![^@${}(),;:<>"\[\]\s]*[@${}(),;:<>"])([\w-]{1,32}\.[\w-]{1,32})(?:\S*)?/g
const mentionRegex = /\[@(\d+)\|([^|\]]+)]/g
const textHtml = text.replace(linkRegex, match => `<a href="${match}">${match}</a>`)
.replace(mentionRegex, '<a href="https://dtf.ru/u/$1">@$2</a>')
.split('\n\n').map(block =>
`<p>${block.split('\n').map((line, idx, arr) =>
line.startsWith('>') ? `<span class="quote">${line.slice(1)}</span>` :
(idx === arr.length - 1 ? line : `${line}<br>`)).join('')}</p>`
).join('')
return `
<div class="comment" style="--branches-count: ${level > 8 ? 8 : level}; --height: auto;">
<div class="comment__branches"></div>
<div class="comment__content" data-content="true">
<div class="author" style="--20398234: 36px;">
<a class="author__avatar" href="${authorUrl}">
${insertAvatar(author.avatar)}
</a>
<div class="author__main">
<a class="author__name" href="${authorUrl}">${author.name}</a>
${isPinned ? `<div class="comment__icon"><svg class="icon icon--pin" width="14" height="14"><use xlink:href="#pin"></use></svg></div>` : ''}
</div>
<div class="author__details"><a class="comment__detail" href="${commentUrl}"><time title="${time.toLocaleString().replace(',', ' в')}" datetime="${time.toISOString()}">${time.toLocaleString().slice(0, 10)}</time></a></div>
</div>
<div class="comment__text">
${textHtml}
</div>
${media && `<div class="comment-media">${media.reduce((acc, cur) => acc + insertMedia(cur), '')}</div>`}
<div class="comment__actions">
<div class="reaction-root">
<button class="reaction reaction--like">
<div class="reaction__icon">
<svg class="icon icon--like" width="20" height="20">
<use xlink:href="#like"></use>
</svg>
</div>
<span>${likes.counterLikes}</span>
</button>
</div>
<a class="comment__action" href="${commentUrl}">Ответить</a>
</div>
</div>
</div>
`
}
function insertAvatar({ data: { uuid, width, height, type, color, base64preview } }) {
const src = `https://leonardo.osnova.io/${uuid}/-/scale_crop/72x72/-/format/webp`
const style = `aspect-ratio: ${width / height}; width: 36px; height: 36px; max-width: none; --background-color: #${color};`
return insertAvatarPicture({ src, style, type, base64preview })
}
function insertAvatarPicture({ src, style }) {
return `
<div data-loaded="false" class="andropov-media andropov-media--rounded andropov-media--bordered andropov-media--has-preview andropov-image" style="${style}">
<picture>
<source srcset="${src}/-/scale_crop/72x72/-/format/webp" type="image/webp">
<img src="${src}/-/scale_crop/72x72/" alt="" loading="lazy">
</picture>
</div>
`
}
function insertMedia({ type, data }) {
switch (type) {
case 'video':
return insertExternalVideoThumbnail(data)
case 'link':
return insertMediaLink(data)
case 'movie':
case 'image':
return data.type === 'gif' || data.type === 'mp4'
? insertMediaVideo(data)
: insertMediaPicture(data)
default:
return insertMediaNotSupported()
}
}
function insertMediaVideo({ uuid, width, height, external_service: { mp4_url } = {} }) {
const src = mp4_url || `https://leonardo.osnova.io/${uuid}/-/format/mp4#t=0.1`
const style = `aspect-ratio: ${width / height}; max-width: 400px; --background-color: #fff;`
return `
<div data-loaded="false" class="andropov-media andropov-media--rounded andropov-media--bordered andropov-media--has-preview andropov-video" style="${style}">
<video preload="metadata" controls="true" playsinline="true" loop="" src="${src}" data-video="0"></video>
</div>
`
}
function insertMediaPicture({ uuid, width, height, color }) {
const src = `https://leonardo.osnova.io/${uuid}`
const style = `aspect-ratio: ${width / height}; max-width: 400px; --background-color: #${color};`
return `
<div data-loaded="false" class="andropov-media andropov-media--rounded andropov-media--bordered andropov-media--has-preview andropov-image andropov-image--zoom" style="${style}">
<picture>
<source srcset="${src}/-/preview/400x/-/format/webp" type="image/webp">
<img src="${src}/-/preview/400x/" alt="" loading="lazy">
</picture>
</div>
`
}
function insertExternalVideoThumbnail({
thumbnail: { data: { uuid, width, height, color, } },
external_service: { name: serviceName }
}) {
const src = `https://leonardo.osnova.io/${uuid}`
const style = `aspect-ratio: ${width / height}; max-width: 400px; --background-color: #${color};`
const youtubeLogo = `
<svg version="1.1" viewBox="0 0 68 48" width="68" height="48">
<path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path>
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
</svg>
`
const coubLogo = `
<svg width="36" height="49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.72 4.123c.11-1.237-.23-2.302-.932-3.055-.695-.745-1.665-1.099-2.644-1.066C14.14.07 12.232 1.69 11.758 4.58c-.76 4.634-.581 12.421.178 20.094a10.56 10.56 0 00-2.7-1.338c-1.673-.561-3.858-.835-5.67-.489-.91.174-1.813.521-2.495 1.157C.357 24.67-.036 25.586.003 26.7c.029.844.583 1.398 1.08 1.733.501.338 1.135.588 1.732.79.384.13.81.259 1.215.381l.004.002.007.002.623.19c.603.19 1.106.368 1.48.562.618.32 1.248.931 1.912 1.8.659.862 1.287 1.888 1.94 2.965l.08.132v.001c.616 1.017 1.257 2.075 1.931 2.978.445.597.936 1.171 1.484 1.638a.996.996 0 00-.089.412v5.391c0 .95.673 1.792 1.642 1.956 4.742.805 10.791.83 14.354.029.926-.209 1.504-1.037 1.504-1.91v-5.37-.023c.288-.175.554-.389.757-.654.484-.633.99-1.746 1.455-2.991.479-1.282.957-2.818 1.36-4.38.402-1.562.734-3.169.915-4.591.176-1.383.228-2.724-.023-3.678-.18-.69-.644-1.252-1.16-1.7-.525-.455-1.186-.862-1.903-1.225-1.436-.729-3.24-1.355-5.001-1.87-2.848-.83-5.697-1.4-6.959-1.634-.342-1.792-1.195-7.144-.624-13.513zm9.182 36.988c-.18.043-.364.084-.552.122-1.405.284-3.127.441-4.87.484-1.747.043-3.548-.028-5.117-.213-1.085-.128-2.12-.316-2.96-.585v4.747c4.538.767 10.258.771 13.5.057v-4.612zM13.732 4.904c.35-2.14 1.61-2.874 2.48-2.903.457-.015.85.148 1.113.43.256.275.47.75.402 1.513-.692 7.714.622 13.981.782 14.71a1.087 1.087 0 00.879.842c.483.083 3.93.696 7.353 1.695 1.716.5 3.383 1.086 4.657 1.732.638.324 1.142.645 1.498.953.365.317.499.555.536.697.145.553.145 1.56-.027 2.917-.167 1.317-.48 2.839-.868 4.344a42.804 42.804 0 01-1.297 4.18c-.464 1.243-.884 2.103-1.17 2.476-.032.042-.183.18-.59.35-.384.158-.898.306-1.526.433-1.255.253-2.853.404-4.523.445-1.667.04-3.372-.028-4.834-.2-1.499-.177-2.623-.45-3.208-.752-.552-.286-1.137-.864-1.779-1.725-.614-.825-1.21-1.809-1.845-2.855l-.002-.003-.057-.095c-.643-1.061-1.326-2.181-2.06-3.142-.73-.953-1.575-1.84-2.582-2.362-.542-.28-1.19-.502-1.801-.694-.24-.075-.472-.145-.697-.213l-.006-.002a33.796 33.796 0 01-1.103-.346c-.568-.193-.988-.373-1.256-.554a.79.79 0 01-.2-.172c-.013-.543.165-.885.435-1.137.308-.287.809-.521 1.505-.654 1.4-.268 3.231-.059 4.659.42 1.55.52 2.508 1.258 3.067 1.805.28.274.468.508.59.664l.016.021c.043.056.13.17.201.241.023.023.096.097.204.164l.003.001c.05.032.485.301.994.038.482-.25.532-.73.538-.784v-.003c.014-.12.003-.22-.001-.253-.99-8.455-1.282-17.335-.48-22.222zM1.991 26.586l.004.01-.005-.01z" fill="#fff"></path>
</svg>
`
const twitchLogo = ``
return `
<div data-loaded="false" class="andropov-media andropov-media--rounded andropov-media--bordered andropov-media--has-preview andropov-external-video andropov-external-video--youtube" style="${style}">
<div data-loaded="false" class="andropov-media andropov-media--has-preview andropov-external-video__cover andropov-image andropov-external-video__cover" style="${style}">
<picture>
<source srcset="${src}/-/preview/700x/-/format/webp" type="image/webp">
<img src="${src}/-/preview/700x/" alt="" loading="lazy">
</picture>
</div>
<div class="andropov-external-video__overlay"></div>
<div class="andropov-external-video__play-button">
${serviceName === 'youtube' ? youtubeLogo : serviceName === 'coub' ? coubLogo : twitchLogo}
</div>
</div>
`
}
function insertMediaLink({ url, title, description, image: { data: { uuid } } }) {
const src = uuid.match(/http/) ? uuid : `https://leonardo.osnova.io/${uuid}`
return `
<a class="andropov-link" href="${url}">
<div class="andropov-link__origin">
<div data-loaded="true" class="andropov-media andropov-media--bordered andropov-media--has-preview andropov-link__icon andropov-image andropov-link__icon" style="aspect-ratio: 1 / 1; width: 16px; height: 16px; max-width: none; --background-color: #fff;">
<picture>
<source srcset="${src}" type="image/webp">
<img src="${src}" alt="" loading="lazy">
</picture>
</div>
<div class="andropov-link__hostname">${new URL(url).hostname}</div>
</div>
<div class="andropov-link__title">${title}</div>
<div class="andropov-link__description">${description}</div>
</a>
`
}
function insertMediaNotSupported() {
return `
<i>Вложение не поддерживается</i>
`
}
} catch (error) {
console.error(`Ошибка при загрузке комментариев (${url}):`, error)
}
}
function getLinkToCss(regexp, fallback = '') {
for (const linkElement of doc.querySelectorAll('link')) {
if (linkElement.href.match(regexp))
return linkElement.href
}
return fallback
}
async function getCachedStyles(url, key) {
try {
const link = url.split('/').slice(-1)[0].split('.')[0]
const cachedStyles = JSON.parse(localStorage.getItem(key) || '{}')
if (cachedStyles[link])
return cachedStyles[link]
const styles = await (await fetchData(url)).text()
localStorage.setItem(key, JSON.stringify({ [link]: styles }))
return styles
} catch (error) {
console.error(`Ошибка при загрузке стилей (${url}):`, error)
}
}
} catch (error) {
console.log('❌ FAILED')
console.error(`Ошибка при загрузке страницы (${url}):`, error)
}
}
async function downloadPagesSequentially(links, index = 0) {
if (index < links.length) {
await downloadPage(links[index], index, links.length)
await downloadPagesSequentially(links, index + 1)
}
}
async function downloadPagesInParallel(links, limit = MAX_PARALLEL_DOWNLOADS) {
const sem = new Semaphore(limit) // Создаем семафор с ограничением
const promises = links.map(async (link, idx) => {
await sem.acquire() // Захватываем один слот семафора
try {
await downloadPage(link, idx, links.length) // Запускаем загрузку страницы
} finally {
sem.release() // Освобождаем слот семафора после завершения загрузки
}
})
await Promise.all(promises) // Ожидаем завершения всех загрузок
}
class Semaphore {
constructor(initialCount) {
this.count = initialCount
this.waitQueue = []
}
acquire() {
return new Promise(resolve => {
if (this.count > 0) {
this.count--
resolve()
} else {
this.waitQueue.push(resolve)
}
})
}
release() {
this.count++
const next = this.waitQueue.shift()
if (next) next()
}
}
async function fetchData(url, headers) {
try {
const response = await fetch(url, { headers })
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 1
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
return await fetchData(url, headers)
}
if (!response.ok)
throw new Error(`status: ${response.status}`)
return {
json: async () => await response.json(),
text: async () => await response.text(),
blob: async () => await response.blob(),
}
} catch (error) {
throw new Error(error)
}
}
function createBookmarksHtml(links) {
let htmlContent = '<!DOCTYPE NETSCAPE-Bookmark-file-1>\n'
htmlContent += '<!-- This is an automatically generated file.\n'
htmlContent += 'It will be read and overwritten.\n'
htmlContent += 'Do Not Edit! -->\n'
htmlContent += '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">\n'
htmlContent += '<TITLE>Bookmarks</TITLE>\n'
htmlContent += '<H1>Bookmarks</H1>\n'
htmlContent += '<DL><p>\n'
const timestamp = new Date().getTime()
htmlContent += `<DT><H3 ADD_DATE="${timestamp}" LAST_MODIFIED="${timestamp}">Закладки с DTF</H3>\n`
htmlContent += '<DL><p>\n' // Открываем раздел "Закладки"
links.forEach(link => {
htmlContent += `<DT><A HREF="${link.url}">${link.title}</A>\n`
})
htmlContent += '</DL><p>\n' // Закрываем раздел "Закладки"
htmlContent += '</DL><p>\n'
// Создаем объект Blob с HTML-содержимым
const blob = new Blob([htmlContent], { type: 'text/html' })
// Создаем ссылку для скачивания файла
const url = URL.createObjectURL(blob)
// Создаем ссылку для скачивания HTML-файла с названием "bookmarks.html"
const a = document.createElement('a')
a.href = url
a.download = `bookmarks_${new Date().toLocaleString().slice(0, 10)}.html`
// Добавляем ссылку на страницу и автоматически кликаем по ней для скачивания файла
document.body.appendChild(a)
a.click()
// Освобождаем ресурсы URL объекта после скачивания файла
URL.revokeObjectURL(url)
}
let prevPage = window.location.href
const pageObserver = new MutationObserver(() => {
if (window.location.href !== prevPage) {
prevPage = window.location.href
setTimeout(setup, DELAY)
}
})
pageObserver.observe(document.body, { childList: true, subtree: true })
setTimeout(setup, DELAY)
Комментарий недоступен
пусть сначала роскомнадзор проверит твой код
вдруг он вредоносный и ты украдешь деньги с карты
Так в этом весь смысл
Краду деньги со своей карты каждый день. Пока никто не просёк
Закладки ищешь? 🤔
А чего с рекламой?