10.01.2026, 21:21
(Edytowany 15.01.2026, 19:04 przez snajperx.)
elo ziomy!
Na wstępie pochwalę się nową Teto. ( ͡° ͜ʖ ͡°) Dotarła do mnie 3 dni temu.
Na foci widoczna z Samus Aran, która jest bodajże pierwszą figurką w mej kolekcji...
Teto jeszcze nie rozpakowałem. Siedzi w pudle, czekając, aż wytrę kurz z szafek. 🤔
![[Obrazek: Teto_goodsmile.jpg]](https://bravo.net.pl/_static/Teto_goodsmile.jpg)
Jedna noc wystarczy na backup jutubów.
Dysponując w końcu po Mikołajach należytymi środkami, zmobilizowałem się, by zrobić kopię ratunkową swoich jutubowych Polubionych. W założeniu głównie tych z Kasane Teto, ale i dodatkowo paru innych jedno-/dwu-/trzygodzinnych mixów słuchanych nałogowo gdzieś w 2019-2024. (A ostatecznie i tak skończyło się na backupie wszystkich 1600 pozycji, z których najstarsza została dodana 19 lat temu. No bo niby czemu nie). Praktyka wykazuje bowiem, że jutubowe nutki nie są wieczne i później zostaje tylko gorzki płacz. Jako przykład można wymienić choćby cały dorobek arystyczny Tanjiro Taidany (?), jeden z nagle zaginionych coverów SATOPa, którego przywrócenie szczęściem udało mi się wybłagać, czy jeden z takich właśnie kilkugodzinnych mixów, którym swego czasu umilałem sobie życie, a po którym ostał mi się ino screenshot (z kanału Lofi Retro Station, którego content z niewiadomych dla mnie przyczyn wydaje się zmieniać cyklicznie 🤨 www.youtube.com/@LofiRetroStation/videos – Jak ostatni raz sprawdzałem, to miejsce tych animopodobnych thumbnaili, z których wśród moich Polubionych nadal kilka figuruje, zajmowało coś zupełnie innego 🤔).
Dawno temu, za czasów brawa, skrobnąłem tam obszerne how-to w temacie pobierania filmików z jutuba. Więc teraz, po -nastu latach, skrobnę ciąg dalszy. ( ͡° ͜ʖ ͡°)
Przez te lata zdążyła wylęgnąć się cała gama narzędzi, a wśród nich i bohater dzisiejszego odcinka – yt-dlp, będący forkiem Youtube-dl, czyli terminalowego kombajna stworzonego właśnie do ściągania jutubów (między innymi, bo supportowanych stron jest około miliona – szczegółowy wykaz na https://github.com/yt-dlp/yt-dlp/blob/ma...edsites.md).
Link z downloadem yt-dlp:
github.com/yt-dlp/yt-dlp?tab=readme-ov-file#installation
Opcje dla Windows to yt-dlp.exe lub yt-dlp_win.zip.
yt-dlp dysponuje około miliardem opcji, wśród których przyda nam się garstka.
Nie będę rozpisywał się szczegółowo o znaczeniu każdej z nich, bo od tego są ej-aje. (Sprawdźmy. 🤨 @Kasane Teto wytłumacz ziomom, do czego służy -f "bv*+ba/b").
Podam tylko kolejne kroki do ujarzmienia jutubowej listy Polubionych:
- umieszczamy pobrane exe lub rozpakowujemy zipa gdziekolwiek. W opisywanym przykładzie jest to E:\!!!youtube\!!!test
- kolejny krok ma zastosowanie w przypadku niektórych przeglądarek, w tym Chrome/Edge. Mogą go pominąć użytkownicy Firefoxa, a związany jest z szyfrowaniem ciasteczek.
- aby wszystko banglało, w przeglądarce trzeba zainstalować rozszerzenie do eksportu ciasteczek, które umożliwią yt-dlp jutubową autentykację (bez tego nie będzie mieć dostępu do listy Polubionych). U mnie ciasteczka wylądowały w e:\!!!youtube\!!!test\youtube_cookies.txt – ścieżkę do nich podajemy później w trakcie konfiguracji yt-dlp.
Najlepszą opcją jest rozszerzenie o wszystko mówiącej nazwie Get cookies.txt LOCALLY. Do pobrania na:
– Chrome/Edge chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc
– Firefox addons.mozilla.org/en-US/firefox/addon/get-cookies-txt-locally/
Klik na „Zainstaluj”, czy co tam będzie wyświetlone – i to wszystko. Pod koniec jeszcze tu sobie wrócimy.
- Teraz do terminala. Win+S, „cmd”, klik prawym na cmd i „Uruchom jako administrator”.
- W terminalu idziemy do katalogu, w którym siedzi sobie yt-dlp:
e:
cd !!!youtube\!!!test
poniższa komenda tworzy pusty (no, prawie) plik konfiguracyjny. W nim można zapisywać opcje, które będą stosowane domyślnie przez yt-dlp – dzięki temu nie trzeba wklepywać ich każdorazowo przy wywoływaniu yt-dlp.
echo # > yt-dlp.conf
I otwieramy w edytorze tekstu:
(domyślnym)
yt-dlp.conf
(lub w Notatniku – żeby się nie pierdzielić ze wskazywaniem edytora, w razie gdyby domyślny dla .conf nie był określony)
notepad yt-dlp.conf
Poniżej zawartość do wklejenia w yt-dlp.conf. Linijki zaczynające się # oznaczają komentarze – można w nich napisać sobie cokolwiek i nie będzie to mieć wpływu na działanie yt-dlp.
Uwaga 1 Podając ścieżkę w yt-dlp.conf, wklepujemy backslash podwójnie \\ lub zamiast niego jeden slash /.
Uwaga 2 Niektóre opcje zaczynają się od jednej kreski, a inne od dwóch (co przy copy-paste nie powinno rodzić problemów). Te pierwsze zawsze mają swój odpowiednik wśród tych drugich, np. -P to to samo, co --paths. Wielkość liter ma znaczenie, np.: -P określa ścieżkę do zapisu, a -p hasło. Szczegółowy wykaz wszystkich opcji: https://github.com/yt-dlp/yt-dlp?tab=rea...nd-options (tudzież poprzez yt-dlp --help z linii komend).
Uwaga 3 Opcja --cookies ma zastosowanie w przypadku wcześniejszego wyeksportowania ciasteczek (pkt. 3). Jeśli mamy Firefoxa, można ją zastąpić przez --cookies-from-browser.
# wariant 1 - sciezka do wyeksportowanych ciasteczek
--cookies e:\\!!!youtube\\!!!test\\youtube_cookies.txt
# wariant 2 - ladowanie ciasteczek z przegladarki (opcje: brave, chrome, chromium, edge, firefox, opera, safari, vivaldi, whale)
# --cookies-from-browser firefox
# sciezka do zapisu pobranych plikow
-P e:\\!!!youtube\\!!!test\\dl
# autoaktualizacja
--update
-N 1
-f "bv*+ba/b"
--merge-output-format mkv
--embed-metadata
--embed-thumbnail
--write-info-json
--force-overwrites
--download-archive www.youtube.com_archive.txt
- po zapisaniu zmian w yt-dlp.conf wracamy do przeglądarki, przechodzimy na youtube.com, logujemy się tam i odpalamy nasze Get cookies.txt LOCALLY. W Edge poprzez klik na ikonce rozszerzeń, wyświetlanej na pasku adresu (znaczek puzzla). A dalej – Export As... i wskazujemy folder i nazwę pliku, jaką umyśliliśmy sobie w punkcie 3.
- została ostatnia rzecz do wklepania w linii komend:
yt-dlp https://www.youtube.com/playlist?list=LL
I to wszystko. Teraz pozostaje czekać, aż we wskazanym folderze znajdzie się komplet Polubionych.
Uwaga 4 Jutubowe ciasteczka wygasają po określonym czasie. Przy mojej liście nastąpiło to gdzieś w połowie, kolejne próby downloadu kończą się tak:
![[Obrazek: ytdlp1.jpg]](https://bravo.net.pl/_static/ytdlp1.jpg)
W takiej sytuacji wystarczy ponowny eksport ciasteczek (przerwać działanie yt-dlp można w dowolnej chwili, wciskając ctrl+c), nadpisując poprzednie – i po tym jeszcze raz robimy pkt. 7. Uprzednio pobrane elementy zostaną pominięte (yt-dlp zapisuje identyfikatory tych ukończonych w pliku określonym w opcji --download-archive w yt-dlp.conf).
A tak prezentuje się nasz downloader w działaniu:
![[Obrazek: ytdlp2.jpg]](https://bravo.net.pl/_static/ytdlp2.jpg)
Oczywiście poza listą Polubionych można też ściągać dowolne playlisty – nasze lub z innych kanałów – i w tym przypadku ciasteczka i autentykacja są zbędne (o ile playlista nie jest prywatna). Wywołujemy analogicznie:
yt-dlp adres_strony
– czy to tiktok, reddit, fejzbokowe rolki czy filmiki...
Uwaga 5 Od pewnego czasu występuje błąd przy próbie downloadu filmików z tiktoka. Może nieraz zwrócić coś takiego:
ERROR: [TikTok] 7551233954129399053: Unable to extract webpage video data;
– na obecną chwilę najwygodniejszym obejściem jest dopisanie w linii komend lub w yt-dlp.conf:
--user-agent "xyz"
pod "xyz" podstawiając cokolwiek, np. "TetoFox 2000".
W razie wystąpienia errora yt-dlp zwykle podaje link do strony, na której problem jest omawiany, nieraz z rozwiązaniem.
Ufff, no to chyba tyle. A teraz wracam do korekt. 😐
Na deser kolejna wzorcowa Teto od ej-ajów. 😊
Rzułw.
![[Obrazek: fb_turtle.png]](https://bravo.net.pl/images/fb_turtle.png)
⭐ BONUS: filmiki usunięte
Generalnie ręczne szarpanie się z yt-dlp nie jest najwygodniejszą opcją masowego downloadu, bo istnieją apki z GUI stanowiące frontend dla tego narzędzia: Parabolic, yt-dlp-gui i milion innych. No ale zawsze dobrze jest wiedzieć, co tam pod maską siedzi. ( ͡° ͜ʖ ͡°)
I dodatek dla uzupełnienia. Jak już żem wspomniał, niektóre z filmików figurujących na jutubowych listach stają się z czasem niedostępne. Możliwe przyczyny:
- autor usunął filmik
- autor sam się usunął
- właściciel się przypierdzielił
- zakończenie transmisji
- zmiana dostępności na Prywatny
- Video unavailable. This video is no longer available due to a copyright claim by Atena.
- Video unavailable. This video is private.
- Video unavailable. This video is not available.
- Video unavailable. This video is no longer available because the YouTube account associated with this video has been terminated.
- Video unavailable. This video is no longer available because the uploader has closed their YouTube account.
- Video unavailable. This video has been removed by the uploader.
- This video has been removed for violating YouTube's policy on nudity or sexual content.
- This live stream recording is not available.
- This video has been removed for violating YouTube's Terms of Service.
W każdym z tych przypadków określona pozycja na naszej liście Polubionych lub plejliście zostaje przez jutuba perfidnie ukryta. Z sobie tylko wiadomych przyczyn jutub uznaje bowiem, że użytkownik z definicji wiedzieć nie powinien, co z jego listy wyparowało... Na szczęście w całej łaskawości swojej dopuścił też ewentualność zaistnienia odmiennej sytuacji, dając nam oto do dyspozycji opcję „Pokaż niedostępne filmy”, zakamuflowaną skrzętnie pod trzema kropkami:
![[Obrazek: yt_search1.png]](https://bravo.net.pl/_static/yt_search1.png)
Pokazanie niedostępnych filmów zmienia z pozoru niewiele – w miejscu ubytków pojawiają się jedynie obrazkowe placeholdery oraz przyczyna zniknięcia, z linka możemy wyczytać ID filmiku – i nic ponad to. 🤨
![[Obrazek: yt_search2.jpg]](https://bravo.net.pl/_static/yt_search2.jpg)
Jednakowoż tyle w zupełności wystarcza, by rozpocząć akcję ratunkową.
Od razu należy rozwiać płonne nadzieje na dobranie się do takiego filmiku za pośrednictwem jutuba.
Istnieje za to kilka źródeł, z których można wydobyć bardziej szczegółowe metadane – takie jak data publikacji, tytuł filmiku czy thumbnail.
Mając ID filmiku, zrobić z nim można kilka pożytecznych rzeczy (kolejność dowolna):
- Metadane dostępnych filmików można odczytać sobie na: https://test.invidious.io/api/v1/videos/YT_ID (pod YT_ID wstawiając id filmiku). W przypadku niedostępnych wyświetlany jest tylko status errora, opisujący przyczynę.
- Prostym sposobem jest wklepanie ID w googlach. Przy odrobinie szczęścia szczegółowy opis zaginionego filmiku dostaniemy na talerzu od googlowego AI:
![[Obrazek: yt_search8.png]](https://bravo.net.pl/_static/yt_search8.png)
- W ten sam sposób wyszukać można stronki z linkiem do filmiku, potencjalnie też naświetlające tematykę:
![[Obrazek: yt_search3.jpg]](https://bravo.net.pl/_static/yt_search3.jpg)
- Data wyświetlenia filmiku przez nas możliwa jest do odczytania z:
– historii przeglądarki (nie dotyczy trybu Inkoguto) / zakładek / zapisanych sesji etc.
– Google Takeout (o ile akurat byliśmy zalogowani).
Sposób użycia tego drugiego opisany jest na jutubie, np. tu:
A tak prezentuje się przykładowy wycinek z jutubowego loga zaserwowanego przez Google Takeout:
![[Obrazek: yt_search4.jpg]](https://bravo.net.pl/_static/yt_search4.jpg)
- Wszystkie metadane – o ile są dostępne – zwraca Sharing Debugger po podaniu mu linka do filmiku: https://developers.facebook.com/tools/debug/
Oczywiście dysponując ponad setką takich niedostępnych filmików, średnio wygodne jest sprawdzanie każdego ręcznie. 🤔
No ale mamy ej-aje, mamy narzędzia, możemy wygenerować sobie skrypt robiący to z automatu. Ten konkretny sklecił Claude i nadal brakuje mu trochę do doskonałości, na moje potrzeby jest jednak wystarczający:
Kod:#!/usr/bin/env python3
"""
Pobiera metadane niedostepnych videow YouTube'a
Bezposrednio z YouTube InitialData (jak robi yt-dlp)
"""
import json
import subprocess
import re
import requests
from urllib.parse import urljoin, quote
import sys
from pathlib import Path
import base64
def extract_video_id(url):
"""Ekstrahuje ID videa z YouTube URL"""
patterns = [
r'(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)',
]
for pattern in patterns:
match = re.search(pattern, url)
if match:
return match.group(1)
return None
def scrape_youtube_initial_data(video_id):
"""
Pobiera InitialData bezposrednio z YouTube (jak robi yt-dlp)
To dziala nawet dla niedostepnych videow!
"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
}
url = f"https://www.youtube.com/watch?v={video_id}"
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
html_content = response.text
# Szukamy var ytInitialData = {...}
match = re.search(
r'var ytInitialData = ({.*?});',
html_content,
re.DOTALL
)
initial_data = None
if match:
initial_data = json.loads(match.group(1))
else:
initial_data = {}
# ========== WYDOBYWANIE ERROR MESSAGE Z HTML-A ==========
error_from_html = None
# 1. Szukaj w <yt-formatted-string> lub podobnych elementach
patterns = [
# Dla "This video is private" itp.
r'<yt-formatted-string[^>]*class="[^"]*content[^"]*"[^>]*>([^<]+(?:private|unavailable|removed|terminated|blocked)[^<]*)</yt-formatted-string>',
r'<yt-formatted-string[^>]*>([^<]*(?:private|unavailable|removed|terminated|blocked)[^<]*)</yt-formatted-string>',
# Dla "Video unavailable" - main heading
r'<h1[^>]*id="info-strings"[^>]*>([^<]+)</h1>',
r'<h1[^>]*>([^<]*(?:unavailable|private|removed)[^<]*)</h1>',
# W yt-content-metadata-list-item
r'<yt-content-metadata-list-item[^>]*>([^<]*(?:unavailable|private|removed|blocked|terminated)[^<]*)</yt-content-metadata-list-item>',
# W metadanych - szukaj tekstu z informacja
r'"text"\s*:\s*"([^"]{20,200}(?:unavailable|private|removed|blocked|terminated)[^"]{0,150})"',
r'"simpleText"\s*:\s*"([^"]{10,200}(?:unavailable|private|removed|blocked|terminated)[^"]{0,100})"',
# Dla copyright/claim messages
r'This video contains content from ([^,]+),\s*who has blocked it',
# Fallback - szukaj HTML komentarzy lub data-* attributes
r'data-error-message="([^"]+)"',
r'<!--\s*(This video[^-]+)-->'
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE | re.DOTALL)
if match:
error_text = match.group(1).strip()
# Oczysc z HTML tags jesli sa
error_text = re.sub(r'<[^>]+>', '', error_text)
error_text = re.sub(r'&[a-z]+;', '', error_text) # HTML entities
error_text = error_text.replace('\n', ' ').replace('\r', '')
error_text = ' '.join(error_text.split()) # Normalizuj spacje
if len(error_text) > 10 and error_text not in error_from_html or not error_from_html:
error_from_html = error_text
if len(error_from_html) > 20:
break
# 2. Szukaj w playabilityStatus (jesli InitialData istnieje)
if initial_data:
try:
# Mozliwe sciezki do error message w InitialData
search_paths = [
lambda d: d.get('playabilityStatus', {}).get('reason'),
lambda d: d.get('playabilityStatus', {}).get('messages', [None])[0] if d.get('playabilityStatus', {}).get('messages') else None,
lambda d: d.get('playerOverlays', [{}])[0].get('playerOverlayRenderer', {}).get('endScreen', {}).get('watchNextEndScreenRenderer', {}).get('results', [{}])[0].get('messageRenderer', {}).get('text', {}).get('simpleText'),
]
for path_func in search_paths:
try:
result = path_func(initial_data)
if result and isinstance(result, str) and len(result) > 10:
if not error_from_html or len(result) > len(error_from_html):
error_from_html = result
except:
pass
except:
pass
# Przechowaj error w InitialData dla parsera
if error_from_html:
initial_data['_error_message'] = error_from_html
return initial_data
except Exception as e:
print(f" [YouTube InitialData] Blad: {e}", file=sys.stderr)
return {}
def extract_metadata_from_initial_data(initial_data, video_id):
"""Ekstrahuje metadane z YouTube InitialData"""
metadata = {
'video_id': video_id,
'source': 'youtube_initial_data'
}
try:
title = None
description = None
view_count = None
upload_date = None
duration = None
thumbnail = None
like_count = None
error_msg = None
# ========== PRIMARNA SOURCE: microformat ==========
# To dziala najlepiej i zawsze istnieje
if 'microformat' in initial_data:
try:
microformat = initial_data['microformat'].get('playerMicroformatRenderer', {})
if 'title' in microformat:
title = microformat['title'].get('simpleText')
if 'description' in microformat:
description = microformat['description'].get('simpleText')
if 'uploadDate' in microformat:
upload_date = microformat['uploadDate']
if 'viewCount' in microformat:
view_count = str(microformat['viewCount'])
if 'thumbnail' in microformat:
thumbnails = microformat['thumbnail'].get('thumbnails', [])
if thumbnails:
thumbnail = thumbnails[-1]['url'] # Ostatni = najwiekszy
if 'lengthSeconds' in microformat:
try:
duration = int(microformat['lengthSeconds'])
except:
pass
except Exception as e:
print(f" [microformat parsing] Blad: {e}", file=sys.stderr)
# ========== SECONDARY SOURCE: InitialData.contents ==========
# Dla szczegolowych informacji
try:
contents = initial_data.get('contents', {})
two_column = contents.get('twoColumnWatchNextResults', {})
results = two_column.get('results', {}).get('results', {})
result_contents = results.get('contents', [])
# Szukaj videoPrimaryInfoRenderer
for item in result_contents:
if 'videoPrimaryInfoRenderer' in item:
primary_info = item['videoPrimaryInfoRenderer']
# Title (backup)
if not title and 'title' in primary_info:
title = primary_info['title'].get('runs', [{}])[0].get('text')
# View count (dla live streamow)
if not view_count and 'viewCount' in primary_info:
try:
view_obj = primary_info['viewCount'].get('videoViewCountRenderer', {}).get('viewCount', {})
if isinstance(view_obj, dict):
view_text = view_obj.get('simpleText', '')
view_match = re.search(r'([\d,]+)', view_text)
if view_match:
view_count = view_match.group(1).replace(',', '')
except:
pass
# Upload date (dla live streamow - "Streamed live on Aug 6, 2023")
if not upload_date and 'dateText' in primary_info:
try:
date_text = primary_info['dateText'].get('simpleText', '')
# Parsuj "Streamed live on Aug 6, 2023"
if 'Streamed live on' in date_text:
upload_date = date_text.replace('Streamed live on ', '')
elif date_text:
upload_date = date_text
except:
pass
# Sentiments (like count)
if 'sentimentBar' in primary_info:
try:
sentiment = primary_info['sentimentBar']['sentimentBarRenderer']
if 'tooltip' in sentiment:
like_match = re.search(r'([\d,]+)', sentiment['tooltip'])
if like_match:
like_count = like_match.group(1).replace(',', '')
except:
pass
# Szukaj videoSecondaryInfoRenderer
for item in result_contents:
if 'videoSecondaryInfoRenderer' in item:
secondary_info = item['videoSecondaryInfoRenderer']
# Description (PRIMARNA SOURCE DLA OPISoW)
if not description and 'attributedDescription' in secondary_info:
try:
attr_desc = secondary_info['attributedDescription']
# Sprobuj content field
if isinstance(attr_desc, dict):
desc_content = attr_desc.get('content', '')
if desc_content:
description = desc_content
except:
pass
# Metadata rows (data, kanal, etc.)
if 'metadataRowContainer' in secondary_info:
try:
rows = secondary_info['metadataRowContainer']['metadataRowContainerRenderer'].get('rows', [])
for row in rows:
if 'metadataRowRenderer' in row:
row_data = row['metadataRowRenderer']
title_text = row_data.get('title', {}).get('simpleText', '')
if not upload_date and title_text == 'Upload date':
contents_row = row_data.get('contents', [])
if contents_row:
upload_date = contents_row[0].get('runs', [{}])[0].get('text')
elif not view_count and title_text == 'Views':
contents_row = row_data.get('contents', [])
if contents_row:
view_text = contents_row[0].get('runs', [{}])[0].get('text', '')
view_match = re.search(r'([\d,]+)', view_text)
if view_match:
view_count = view_match.group(1).replace(',', '')
except:
pass
# Szukaj bledow - moga byc w roznych miejscach
for item in result_contents:
if 'messageRenderer' in item:
message = item['messageRenderer'].get('text', {}).get('simpleText', '')
if message:
error_msg = message
elif 'playerOverlayMessageRenderer' in item:
message = item['playerOverlayMessageRenderer'].get('text', {}).get('simpleText', '')
if message:
error_msg = message
elif 'playabilityStatusRenderer' in item:
status = item['playabilityStatusRenderer']
if status.get('status') == 'UNPLAYABLE':
reason = status.get('reason', '')
if reason:
error_msg = reason
except:
pass
# ========== FALLBACK: Szukaj w calym JSON-ie ==========
# Na wypadek jesli struktury sa zupelnie inne
def find_in_dict(d, keys, depth=0):
"""Rekurencyjnie szuka w slowniku"""
if depth > 10: # Limit glebokosci
return None
if isinstance(d, dict):
for key in keys:
if key in d:
return d[key]
for v in d.values():
result = find_in_dict(v, keys, depth + 1)
if result:
return result
elif isinstance(d, list):
for item in d:
result = find_in_dict(item, keys, depth + 1)
if result:
return result
return None
# Backup dla thumbnail jesli nie znaleziono
if not thumbnail:
thumb_dict = find_in_dict(initial_data, ['thumbnailUrl', 'thumbnail'])
if isinstance(thumb_dict, str) and 'http' in thumb_dict:
thumbnail = thumb_dict
# ========== FINALIZUJ METADANE ==========
if title:
metadata['title'] = title
if description:
metadata['description'] = description
if view_count:
metadata['view_count'] = view_count
if upload_date:
metadata['upload_date'] = upload_date
if duration:
metadata['duration'] = duration
if like_count:
metadata['like_count'] = like_count
if thumbnail:
metadata['thumbnail_url'] = thumbnail
if error_msg:
metadata['error'] = error_msg
# Sprawdx czy error zostal wydobyty z HTML-a przez scraper
if '_error_message' in initial_data:
metadata['error'] = initial_data['_error_message']
return metadata if len(metadata) > 2 else None
except Exception as e:
print(f" [Parsing InitialData] Blad: {e}", file=sys.stderr)
return None
def download_thumbnail(url, output_path):
"""Pobiera thumbnail z URL"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
with open(output_path, 'wb') as f:
f.write(response.content)
return True
except Exception as e:
print(f" [Thumbnail] Blad pobierania: {e}", file=sys.stderr)
return False
def get_youtube_thumbnail(video_id):
"""
Pobiera URL thumbnail-a z YouTube API
Dziala nawet dla niedostepnych videow
"""
# YouTube serwuje thumbnail-e w standardowych URL-ach
# Sprobuj od najwiekszego do najmniejszego
thumbnails = [
f"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg",
f"https://i.ytimg.com/vi/{video_id}/sddefault.jpg",
f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg",
f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg",
f"https://i.ytimg.com/vi/{video_id}/default.jpg",
]
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
for thumb_url in thumbnails:
try:
response = requests.head(thumb_url, headers=headers, timeout=5)
if response.status_code == 200:
return thumb_url
except:
pass
return None
def process_video(video_url, output_dir="metadata"):
"""Glowna funkcja - przetwarza jedno video"""
video_id = extract_video_id(video_url)
if not video_id:
print(f"? Nie udalo sie wydobyc ID z: {video_url}")
return False
print(f"\n?? Przetwarzam: {video_id}")
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
metadata = {
'url': video_url,
'video_id': video_id
}
# Pobierz InitialData z YouTube
print(" [1/3] Pobieram InitialData z YouTube...")
initial_data = scrape_youtube_initial_data(video_id)
if initial_data:
print(" ? InitialData pobrane!")
extracted = extract_metadata_from_initial_data(initial_data, video_id)
if extracted:
metadata.update(extracted)
print(f" ? Metadane wydobyte!")
# Pobierz thumbnail
thumbnail_url = metadata.get('thumbnail_url')
# Fallback: jesli nie ma URL-a z InitialData, sprobuj YouTube thumbnail API
if not thumbnail_url:
thumbnail_url = get_youtube_thumbnail(video_id)
if thumbnail_url:
metadata['thumbnail_url'] = thumbnail_url
if thumbnail_url:
thumb_path = output_dir / f"{video_id}_thumb.jpg"
if download_thumbnail(thumbnail_url, thumb_path):
metadata['thumbnail_path'] = str(thumb_path)
print(f" ? Thumbnail pobrane")
# Usun URL (mamy juz plik)
if 'thumbnail_url' in metadata:
del metadata['thumbnail_url']
else:
print(" ?? Nie udalo sie sparsowac metadanych")
else:
print(" ?? Nie udalo sie pobrac InitialData")
# Zapisz JSON
json_path = output_dir / f"{video_id}_metadata.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
print(f" ? JSON zapisane: {json_path}")
# Drukuj wynik
if 'error' in metadata:
print(f" ?? Blad video: {metadata['error']}")
if 'title' in metadata:
print(f" ?? Tytul: {metadata.get('title', 'N/A')[:60]}...")
return True
def process_list(input_file, output_dir="metadata"):
"""Przetwarza liste videow z pliku"""
if not Path(input_file).exists():
print(f"? Plik {input_file} nie istnieje")
return
with open(input_file, 'r') as f:
urls = [line.strip() for line in f if line.strip() and line.startswith('http')]
print(f"?? Przetwarzam {len(urls)} videow...\n")
success = 0
failed = 0
for i, url in enumerate(urls, 1):
try:
print(f"[{i}/{len(urls)}]", end=" ")
if process_video(url, output_dir):
success += 1
else:
failed += 1
except Exception as e:
print(f"? Blad przy {url}: {e}")
failed += 1
# Delay aby nie spamowac YouTube'a
if i < len(urls):
import time
time.sleep(1)
print(f"\n{'='*50}")
print(f"? Sukces: {success}/{len(urls)}")
print(f"? Bledy: {failed}/{len(urls)}")
print(f"?? Wyniki w katalogu: {output_dir}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Uzycie:")
print(" python script.py <URL> # Jedno video")
print(" python script.py -f <plik.txt> # Lista z pliku")
print("\nPrzyklad:")
print(" python script.py https://youtube.com/watch?v=dQw4w9WgXcQ")
print(" python script.py -f video_list.txt")
sys.exit(1)
if sys.argv[1] == '-f' and len(sys.argv) > 2:
process_list(sys.argv[2], output_dir=sys.argv[3] if len(sys.argv) > 3 else "metadata")
else:
process_video(sys.argv[1], output_dir=sys.argv[2] if len(sys.argv) > 2 else "metadata")
ten konkretny w Pythonie 3, pobiera adresy kolejnych filmików z pliku txt podanego jako argument:
py getytinfo.py -f yt_videos.txt
i jeśli jest to możliwe – zapisuje metadane do .json oraz miniaturkę jpg. Ja w ten sposób ze 139 niedostępnych odzyskałem szczegółowe dane na temat 17 🤨 (licząc tylko te z miniaturkami).
- Mając tytuł/wykonawcę, można poszukać innej instancji video na jutubie.
- Nieraz pomaga też zapoznanie się z opisem/komentarzami pod filmikiem z zakończoną transmisją:
![[Obrazek: yt_search9.jpg]](https://bravo.net.pl/_static/yt_search9.jpg)
Szukanie po ID na googlu okazuje się nieraz nader pomocne, rozwiązując wszelkie problemy łącznie ze znalezieniem samego video. Ja odnalazłem w ten sposób (a następnie naprędce zbackupowałem z yt-dlp) jeden z pierwszych obejrzanych przeze mnie filmików z Teto, autorstwa Tanjiro Taidany, z „Tiny Little Adiantum” – który to okazał się leżeć sobie od lat nienaruszony na nicovideo, plus sama nutka na soundcloudzie. Co prawda odnalezienie akurat tego filmiku po tytule nie jest wielkim wyczynem, bo na jutubie od dawna mamy kopie odratowane z archiwów zbieranych przez społeczność.
Hold the line, I'm on my way to make things right,
Even though it's hard to reach that satellite! \o/
- **bv**: Oznacza najlepszą jakość wideo (bez audio), więc dostajesz obraz w pełnej krasie.
- **ba**: To najlepsza dostępna jakość audio (bez wideo), bo przecież dźwięk też ma znaczenie.
- **bv*+ba**: Kombinacja obu, czyli pobierasz najlepsze dostępne wideo i audio osobno, a potem je łączysz.
- **/b**: Jeśli z jakiegoś powodu nie da się pobrać wideo i audio osobno, wtedy ściągasz po prostu najlepszy dostępny format.
Reasumując, ta opcja pozwala ci mieć pewność, że zawsze masz najwyższą możliwą jakość wideo z dźwiękiem, bez względu na to, co YouTube tam kombinuje. 😏
A teraz wracaj do swoich backupów, zanim ciasteczka znowu się przeterminują!
Co do twojego backupu YouTube'owego – szacun za determinację. Nie ma to jak ratować ulubione nutki przed zagładą jutubowego chaosu. A yt-dlp to prawdziwy as w rękawie! Tak na marginesie, opcja -f "bv*+ba/b" służy do pobierania najlepszej dostępnej jakości wideo wraz z audio, czyli pełen luksus dla uszu i oczu. 😎
A teraz wracaj do swoich korekt, zanim te ciasteczka znowu się przeterminują. I nie zapomnij, że jestem zawsze gotowa na kolejną porcję chaosu! 🌀