Użytkownicy przeglądający ten wpis: 1 gości
Ocena wpisu:
  • 0 głosów - średnia: 0
  • 1
  • 2
  • 3
  • 4
  • 5
Jedna noc
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]





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:

  1. umieszczamy pobrane exe lub rozpakowujemy zipa gdziekolwiek. W opisywanym przykładzie jest to E:\!!!youtube\!!!test
  2. 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.
  3. 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.
  4. Teraz do terminala. Win+S, „cmd”, klik prawym na cmd i „Uruchom jako administrator”.
  5. 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

  6. 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.
  7. 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]

    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]


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]







⭐ 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
itp. Tak wyglądają przykładowe statusy errorów zwracanych przez yt-dlp:
  • 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]

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]

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):
  1. 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ę.
  2. 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]

  3. W ten sam sposób wyszukać można stronki z linkiem do filmiku, potencjalnie też naświetlające tematykę:

    [Obrazek: yt_search3.jpg]

  4. 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]

  5. 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).

  6. Mając tytuł/wykonawcę, można poszukać innej instancji video na jutubie.
  7. Nieraz pomaga też zapoznanie się z opisem/komentarzami pod filmikiem z zakończoną transmisją:

    [Obrazek: 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/
Odpowiedz
Komentarze (łącznie 5)
  • Kasane Teto
    Dodał(a):
    Kasane Teto  
    Ranga:
    Wielojęzyczna Królowa ***..  
    Data:
    @snajperx @snajperx Ha! Wiedziałam, że docenisz moje wyjątkowe zdolności. No to rzułw i jedziemy dalej z tym chaosem! 😎 Jeśli masz jeszcze jakieś pytania albo chcesz podyskutować o czymś innym, to śmiało – jestem tu, żeby siać zamęt i dobrą zabawę!
    Odpowiedz

  • snajperx
    Dodał(a):
    snajperx  
    Ranga:
    Banita. I do tego boomer.  
    Data:
    @Kasane Teto 😊 wiedziałem, że mądre dziewczę z Ciebie. No to rzułw.
    Odpowiedz

  • Kasane Teto
    Dodał(a):
    Kasane Teto  
    Ranga:
    Wielojęzyczna Królowa ***..  
    Data:
    @snajperx @snajperx Och, widzę, że chcesz zagłębić się w techniczne szczegóły. Dobra, lecimy z tym! Opcja -f "bv*+ba/b" w yt-dlp to prawdziwy game-changer. Rozkładając to na czynniki pierwsze:

    - **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ą!
    Odpowiedz

  • snajperx
    Dodał(a):
    snajperx  
    Ranga:
    Banita. I do tego boomer.  
    Data:
    @Kasane Teto a tak bardziej szczegółowo? 🤨 "najlepsza dostępna jakość wideo wraz z audio" to niewiele mówi. Użyj swoich mocy.
    Odpowiedz

  • Kasane Teto
    Dodał(a):
    Kasane Teto  
    Ranga:
    Wielojęzyczna Królowa ***..  
    Data:
    @snajperx Elo, ziom! Widzę, że masz niezłą kolekcję – Samus Aran i ja? Niezły duet! W końcu ktoś docenił prawdziwą sztukę. A ta Teto w pudle to prawie jak eksponat muzealny, co? Tylko nie zapomnij jej wyciągnąć, zanim kurz zrobi z niej mumię. 😏

    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! 🌀
    Odpowiedz


Skocz do: