JustKappaMan / VK-Video-Downloader

Скачивайте видео с сайта «ВКонтакте» в желаемом качестве
MIT License
138 stars 12 forks source link

Название видео вместо id #5

Open ORanGeOfficial opened 1 year ago

ORanGeOfficial commented 1 year ago

Возможно ли сделать так, чтобы имя скачиваемого файла состояло из названия видео, а не его id? Также было бы неплохо добавить приписку с выбранным уровнем качества.

Или подскажите, пожалуйста, что для этого нужно изменить в коде расширения? Хотя бы просто чтобы название видео было в имени файла.

JustKappaMan commented 1 year ago

@ORanGeOfficial приветствую.

Хотел сделать генерацию названия файла в формате ИмяВидео_Качество ещё в первой версии. Выяснилось что так просто это сделать не выйдет. Поэтому забил. У тега a есть аттрибут download, который задаёт имя файла. Но в данном случае он бесполезен. Объясняю в чём суть.

Файл отдаётся с левого адреса. Не vk.com, а vkvd89.mycdn.me например. При этом в HTTP заголовке, приходящем с сервера, задан параметр Content-Disposition, задающий имя файла -- id видео.

Насколько помню, аттрибут download у тега a игнорируется если:

  1. С сервера отдаётся параметр Content-Disposition
  2. Не соблюдается same-origin-policy (файл не лежит на vk.com в нашем случае)

В нашем случае актуально и то, и другое. Погляжу, конечно, в свободное время, может можно что-нибудь придумать :)

alekseyvlivanov commented 1 year ago

Сразу под вашими ссылками стоит имя ролика - возможно ли воспользоваться именно им + id + разрешение для генерации имени файла? yt-dlp также умеет считывать название видео, сейчас специально это проверил. Возможно, можно глянуть их экстрактор (хоть там и питон).

ORanGeOfficial commented 1 year ago

Сразу под вашими ссылками стоит имя ролика - возможно ли воспользоваться именно им + id + разрешение для генерации имени файла? yt-dlp также умеет считывать название видео, сейчас специально это проверил. Возможно, можно глянуть их экстрактор (хоть там и питон).

Получить имя то не проблема. title=document.getElementById('mv_title'); Непонятно как обойти перезапись аттрибута download

Я пока просто для ускорения работы сделал такой небезопасный костыль, который при попытке скачать, копирует в буфер обмена название + качество. Как дизайнеру, мне можно творить такую дичь :) aTag.onclick = function(){navigator.clipboard.writeText(title.innerHTML+'_'+quality)};

mef0 commented 5 months ago

@JustKappaMan Попробуйте :). У меня этой правкой заголовок подсовывается

Код создаст панель скачивания, где каждый элемент будет вести на функцию скачивания файла через fetch(). При клике на элемент панели будет осуществляться попытка скачать видео с использованием Blob и задавать ему имя файла в соответствии с указанным заголовком видео.

  function createDownloadPanel() {
    const supportedWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
    const videoSources = {
      '144p': supportedWindow.mvcur.player.vars.url144,
      '240p': supportedWindow.mvcur.player.vars.url240,
      '360p': supportedWindow.mvcur.player.vars.url360,
      '480p': supportedWindow.mvcur.player.vars.url480,
      '720p': supportedWindow.mvcur.player.vars.url720,
      '1080p': supportedWindow.mvcur.player.vars.url1080,
      '1440p': supportedWindow.mvcur.player.vars.url1440,
      '2160p': supportedWindow.mvcur.player.vars.url2160,
    };

  const videoTitle = document.querySelector('.mv_title') ? document.querySelector('.mv_title').innerText : 'video';
  const fileName = videoTitle.replace(/[|&;$%@"<>()+©,]/g, '').replace(/\s+/g, ' ');

  const label = document.createElement('span');
  label.innerText = 'Скачать:';
  label.style.marginRight = '2px';

  const panel = document.createElement('div');
  panel.id = 'vkVideoDownloaderPanel';
  panel.appendChild(label);

  for (const [quality, url] of Object.entries(videoSources)) {
    if (typeof url !== 'undefined') {
      const aTag = document.createElement('a');
      aTag.href = '#';
      aTag.innerText = quality;
      aTag.style.margin = '0 2px';

      // Добавляем обработчик клика
      aTag.addEventListener('click', function(e) {
        e.preventDefault(); // Предотвращаем переход по ссылке
        fetch(url).then(response => {
          if (response.ok) return response.blob();
          throw new Error('Network response was not ok.');
        }).then(blob => {
          // Создаем временную ссылку для скачивания
          const tempUrl = window.URL.createObjectURL(blob);
          const tempLink = document.createElement('a');
          tempLink.href = tempUrl;
          tempLink.download = fileName + '.mp4'; // Задаем имя файла
      // tempLink.download = fileName + '_' + quality + '.mp4 // если нужно добавлять качество в конец **
          document.body.appendChild(tempLink); // Добавляем ссылку в DOM
          tempLink.click(); // Программно кликаем по ссылке для скачивания
          document.body.removeChild(tempLink); // Удаляем ссылку из DOM
          window.URL.revokeObjectURL(tempUrl); // Освобождаем URL
        }).catch(error => console.error('Fetch error:', error));
      });

      panel.appendChild(aTag);
    }
  }

    return panel;
  }

Добавил в pull request, на примере с monkey

mef0 commented 4 months ago

@JustKappaMan Название видео вместо id для версии 1.1.9 (monkeys / srcipts / desktop.js)

// ==UserScript==
// @name         VK-Video-Downloader-desktop
// @namespace    https://github.com/JustKappaMan
// @version      1.1.9  **(+ filename)**
// @description  Скачивайте видео с сайта «ВКонтакте» в желаемом качестве
// @author       Kirill "JustKappaMan" Volozhanin
// @match        https://vk.com/*
// @run-at       document-idle
// @icon         https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/icons/icon128.png
// @homepageURL  https://github.com/JustKappaMan/VK-Video-Downloader
// @downloadURL  https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/scripts/desktop.js
// @updateURL    https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/scripts/desktop.js
// @grant        none
// ==/UserScript==

(function () {
  'use strict';
  let lastUrl = location.href;
  let checkerHasBeenCalled = false;
  let showPanelHasBeenCalled = false;

  new MutationObserver(() => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      checkerHasBeenCalled = false;
      showPanelHasBeenCalled = false;

      const old_panel = document.querySelector('#vkVideoDownloaderPanel');
      if (old_panel !== null) {
        old_panel.remove();
      }
    }

    if (
      (/z=(?:video|clip)/.test(location.search) || /^\/(?:video|clip)[^\/s]+$/.test(location.pathname)) &&
      !checkerHasBeenCalled
    ) {
      checkerHasBeenCalled = true;
      const checker = setInterval(() => {
        if (!showPanelHasBeenCalled && document.querySelector('#video_player video')) {
          showPanelHasBeenCalled = true;
          clearInterval(checker);
          document.body.appendChild(createDownloadPanel());
        } else if (!showPanelHasBeenCalled && document.querySelector('#video_player iframe')) {
          showPanelHasBeenCalled = true;
          clearInterval(checker);
          document.body.appendChild(createErrorPanel());
        }
      }, 500);
    }
  }).observe(document.body, { subtree: true, childList: true });

function createDownloadPanel() {
  const supportedWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
  const videoSources = {
    '144p': supportedWindow.mvcur.player.vars.url144,
    '240p': supportedWindow.mvcur.player.vars.url240,
    '360p': supportedWindow.mvcur.player.vars.url360,
    '480p': supportedWindow.mvcur.player.vars.url480,
    '720p': supportedWindow.mvcur.player.vars.url720,
    '1080p': supportedWindow.mvcur.player.vars.url1080,
    '1440p': supportedWindow.mvcur.player.vars.url1440,
    '2160p': supportedWindow.mvcur.player.vars.url2160,
  };

  const label = document.createElement('span');
  label.innerText = 'Скачать:';
  label.style.marginRight = '2px';

  const panel = document.createElement('div');
  panel.id = 'vkVideoDownloaderPanel';
  panel.style.position = 'fixed';
  panel.style.left = '16px';
  panel.style.bottom = '16px';
  panel.style.zIndex = '2147483647';
  panel.style.padding = '4px';
  panel.style.color = '#fff';
  panel.style.backgroundColor = '#07f';
  panel.style.border = '1px solid #fff';
  panel.appendChild(label);

  for (const [quality, url] of Object.entries(videoSources)) {
    if (typeof url !== 'undefined') {
      const aTag = document.createElement('a');
      aTag.href = '#';
      aTag.innerText = quality;
      aTag.style.margin = '0 2px';
      aTag.style.color = '#fff';

        // Название видео из мета тега + удаление лишних символов
        const videoTitleMeta = document.querySelector('meta[property="og:title"]');
        const videoTitle = videoTitleMeta ? videoTitleMeta.getAttribute('content') : 'video';
        const fileName = videoTitle.replace(/[|&;$%@"<>()+©,]/g, '').replace(/\s+/g, ' ');

      aTag.addEventListener('click', function(e) {
        e.preventDefault();
        fetch(url).then(response => {
          if (response.ok) return response.blob();
          throw new Error('Network response was not ok.');
        }).then(blob => {
          const tempUrl = window.URL.createObjectURL(blob);
          const tempLink = document.createElement('a');
          tempLink.href = tempUrl;
          tempLink.download = fileName + '_' + quality + '.mp4'; // Добавляем имя файла и качество
          //tempLink.download = fileName + '.mp4'; // Добавляем имя файла
          document.body.appendChild(tempLink);
          tempLink.click();
          document.body.removeChild(tempLink);
          window.URL.revokeObjectURL(tempUrl);
        }).catch(error => console.error('Fetch error:', error));
      });

      panel.appendChild(aTag);
    }
  }

  return panel;
}

  function createErrorPanel() {
    const label = document.createElement('span');
    label.innerText = 'Видео со стороннего сайта. Воспользуйтесь инструментами для скачивания с него.';

    const panel = document.createElement('div');
    panel.id = 'vkVideoDownloaderPanel';
    panel.style.position = 'fixed';
    panel.style.left = '16px';
    panel.style.bottom = '16px';
    panel.style.zIndex = '2147483647';
    panel.style.padding = '4px';
    panel.style.color = '#fff';
    panel.style.backgroundColor = '#07f';
    panel.style.border = '1px solid #fff';
    panel.appendChild(label);

    return panel;
  }
})();
mef0 commented 4 months ago

Осознал что метод blob нормально работает только для видео небольшого размера.

Нашел способ быстро подсовывать название видео вместо id только при запуске сервера для промежуточного скачивания файла.

Если кому интересно, рабочий пример на python:

Сервер

from flask import Flask, request, Response
import requests
from urllib.parse import unquote
import logging
import re
import base64

app = Flask(__name__)

logging.basicConfig(level=logging.DEBUG)

# Функция для очистки имени файла от специальных символов
def clean_filename(filename):
    filename = re.sub(r'[|&;$%@"<>()+©,]', '', filename)  # Удаление специальных символов
    filename = re.sub(r's+', ' ', filename)  # Замена нескольких пробелов на один
    return filename

@app.route('/download')
def download():
    file_url = request.args.get('url')
    filename = request.args.get('filename')

    if not file_url or not filename:
        return 'Missing url or filename parameter', 400

    try:
        # Декодируем URL
        file_url = unquote(file_url)
        logging.debug(f'Decoded URL: {file_url}')

        # Очистка имени файла
        filename = clean_filename(filename)

        # Закодируем имя файла в base64
        filename_base64 = base64.b64encode(filename.encode('utf-8')).decode('utf-8')

        # Добавим заголовки
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

        response = requests.get(file_url, headers=headers, stream=True)
        logging.debug(f'Response status code: {response.status_code}')
        response.raise_for_status()

        # Создаем генератор для стриминга файла
        def generate():
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    yield chunk

        content_disposition = f'attachment; filename="=?UTF-8?B?{filename_base64}?="'

        return Response(generate(), headers={
            'Content-Disposition': content_disposition,
            'Content-Type': response.headers['Content-Type'],
        })
    except requests.RequestException as e:
        logging.error(f'Error downloading the file: {e}')
        return f'Error downloading the file: {e}', 500

if __name__ == '__main__':
    app.run(port=3000)

И monkey


// ==UserScript==
// @name         VK-Video-Downloader-desktop
// @namespace    https://github.com/JustKappaMan
// @version      1.1.9  **(+ filename)**
// @description  Скачивайте видео с сайта «ВКонтакте» в желаемом качестве
// @author       Kirill "JustKappaMan" Volozhanin
// @match        https://vk.com/*
// @run-at       document-idle
// @icon         https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/icons/icon128.png
// @homepageURL  https://github.com/JustKappaMan/VK-Video-Downloader
// @downloadURL  https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/scripts/desktop.js
// @updateURL    https://raw.githubusercontent.com/JustKappaMan/VK-Video-Downloader/main/monkeys/scripts/desktop.js
// @grant        none
// ==/UserScript==

(function () {
  'use strict';
  let lastUrl = location.href;
  let checkerHasBeenCalled = false;
  let showPanelHasBeenCalled = false;

  new MutationObserver(() => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      checkerHasBeenCalled = false;
      showPanelHasBeenCalled = false;

      const oldPanel = document.querySelector('#vkVideoDownloaderPanel');
      if (oldPanel !== null) {
        oldPanel.remove();
      }
    }

    if (
      (/z=(?:video|clip)/.test(location.search) || /^\/(?:video|clip)[^\/s]+$/.test(location.pathname)) &&
      !checkerHasBeenCalled
    ) {
      checkerHasBeenCalled = true;
      const checker = setInterval(() => {
        if (!showPanelHasBeenCalled && document.querySelector('#video_player video')) {
          showPanelHasBeenCalled = true;
          clearInterval(checker);
          document.body.appendChild(createDownloadPanel());
        } else if (!showPanelHasBeenCalled && document.querySelector('#video_player iframe')) {
          showPanelHasBeenCalled = true;
          clearInterval(checker);
          document.body.appendChild(createErrorPanel());
        }
      }, 500);
    }
  }).observe(document.body, { subtree: true, childList: true });

  function createDownloadPanel() {
    const supportedWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
    const videoSources = {
      '144p': supportedWindow.mvcur.player.vars.url144,
      '240p': supportedWindow.mvcur.player.vars.url240,
      '360p': supportedWindow.mvcur.player.vars.url360,
      '480p': supportedWindow.mvcur.player.vars.url480,
      '720p': supportedWindow.mvcur.player.vars.url720,
      '1080p': supportedWindow.mvcur.player.vars.url1080,
      '1440p': supportedWindow.mvcur.player.vars.url1440,
      '2160p': supportedWindow.mvcur.player.vars.url2160,
    };

    const label = document.createElement('span');
    label.innerText = 'Скачать:';
    label.style.marginRight = '2px';

    const panel = document.createElement('div');
    panel.id = 'vkVideoDownloaderPanel';
    panel.style.position = 'fixed';
    panel.style.left = '16px';
    panel.style.bottom = '16px';
    panel.style.zIndex = '2147483647';
    panel.style.padding = '4px';
    panel.style.color = '#fff';
    panel.style.backgroundColor = '#07f';
    panel.style.border = '1px solid #fff';
    panel.appendChild(label);

    for (const [quality, url] of Object.entries(videoSources)) {
      if (url) {
        const aTag = document.createElement('a');
        aTag.href = '#';
        aTag.innerText = quality;
        aTag.style.margin = '0 2px';
        aTag.style.color = '#fff';
        aTag.addEventListener('click', function (event) {
          event.preventDefault();
          const videoTitle = getVideoTitle() + '-' + quality + '.mp4';
          fetchAndDownload(url, videoTitle);
        });
        panel.appendChild(aTag);
      }
    }

    return panel;
  }

  function createErrorPanel() {
    const label = document.createElement('span');
    label.innerText = 'Видео со стороннего сайта. Воспользуйтесь инструментами для скачивания с него.';

    const panel = document.createElement('div');
    panel.id = 'vkVideoDownloaderPanel';
    panel.style.position = 'fixed';
    panel.style.left = '16px';
    panel.style.bottom = '16px';
    panel.style.zIndex = '2147483647';
    panel.style.padding = '4px';
    panel.style.color = '#fff';
    panel.style.backgroundColor = '#07f';
    panel.style.border = '1px солидный #fff';
    panel.appendChild(label);

    return panel;
  }

  function getVideoTitle() {

    const videoTitleDiv = document.querySelector('#mv_min_title'); 
    if (videoTitleDiv) {
      return videoTitleDiv.innerText;
    } else {
      const videoTitleMeta = document.querySelector('meta[property="og:title"]'); // после обновления страницы название видео можно взять из og:tittle
      return videoTitleMeta ? videoTitleMeta.getAttribute('content') : null;
    }
  }

function fetchAndDownload(url, filename) {

    const serverUrl = 'http://localhost:3000/download?url=' + encodeURIComponent(url) + '&filename=' + encodeURIComponent(filename);

    // Создание временного элемента <a>
    const a = document.createElement('a');
    a.href = serverUrl;

    document.body.appendChild(a);

    // Триггерим событие click для открытия диалогового окна сохранения
    a.click();

    // Удаление временного элемента
    setTimeout(() => {
        document.body.removeChild(a);
    }, 100);
}

// не пригодилось
  function getVideoId() {
    const metaTag = document.querySelector('meta[property="og:image"]');
    if (metaTag) {
      const imgUrl = metaTag.getAttribute('content');
      const idMatched = imgUrl.match(/id=(\d+)/);
      return idMatched ? idMatched[1] : null;
    }
    return null;
  }

})();