Blog

Bagaimana Jika Kita Dapat Mencari Kata Kunci Apapun dari Video YouTube Manapun Melalui Captionnya?

· 17min

Lompat ke artikel utama.

Demo Pencarian Teks Tertutup YouTube

YouTube video thumbnail

"Judul Video" oleh Nama Kanal

Catatan:
  • Hanya video YouTube yang memiliki subtitle berbahasa Inggris yang didukung.
  • Tidak dapat digunakan pada video YouTube berhak cipta, seperti musik video.
  • Setiap pranala YouTube memerlukan waktu yang lebih lama saat pertama kali dicari karena temboloknya (cache) belum tersedia.
  • Untuk mengurangi penggunaan data, fitur demo otomatis hanya berjalan jika bagian demo ini terlihat di layar peramban (browser) teman-teman.

Tidak sedang mencari kata kunci apapun, coba masukkan sebuah kata kunci.

Timer: 15s


0 hasil pencarian ditemukan. Saat ini berada pada halaman ke-0


    Rangkuman

    Tahun lalu saya mendapatkan ide untuk membuat suatu aplikasi web yang dapat mencari kata kunci dari perkataan orang di video YouTube. Ide tersebut muncul ketika saya sedang menonton web.dev Live. Aplikasi web tersebut tidak menggunakan teknologi yang canggih dan terkini, hanya melakukan pencarian dari caption-nya.

    Ketika kita mengaktifkan fitur caption pada YouTube, halaman YouTube akan melakukan request data dari API. Responnya berupa objek JSON yang dapat kita ubah menjadi format lain, seperti WebVTT. Sayangnya, API tersebut dilengkapi dengan beberapa parameter yang memiliki identitas unik dan waktu kadaluarsa.

    Karena hal tersebut, kita tidak bisa mengakses pranala API tersebut dengan sering. Untungnya, setiap video YouTube memiliki identitas unik di pranala nya. Tidak hanya pada halaman YouTube biasa, tetapi juga pada versi embedded-nya. Kita dapat menggunakan puppeteer untuk mengakses halaman embedded dan meng-intercept request-nya untuk mendapatkan pranala API caption tersebut.

    Kemudian kita dapat memproses data caption menggunakan beberapa paket NPM bersumber terbuka dan mengubahnya menjadi REST API kita sendiri. API ini hanya membutuhkan pranala video YouTube dan sebuah kata kunci. Kita juga menggunakan Vercel sebagai tempat hosting layanan API yang gratis.

    Teman-teman dapat mempelajari lebih lanjut dokumentasi API-nya pada API Cari Teks Video dan daftar proyek-proyek keren yang telah menggunakan API ini pada Awesome Cari Teks Video. Teman-teman juga dapat membaca artikel ini lebih lanjut untuk mempelajari bagaimana implementasinya.

    Awal Mula

    Pada tanggal 30 Juni sampai 2 Juli 2020 tahun kemarin, tim Chrome Developer Advocate mengadapak acara yang sangat spektakuler untuk pengembang web. Acara tersebut bernama web.dev Live.

    Saya sedang melihat acara tersebut hingga suatu saat salah satu Chrome Developer Advocate, Sam Dutton membagikan sebuah pranala di kotak chat. Saya klik pranala tersebut dan ternyata link tersebut berisi aplikasi untuk mencari, menjelajahi caption dari video acara web.dev Live.

    Aplikasi tersebut bernama Search web.dev LIVE. Awalnya saya terkesima karena kita dapat mencari sesuatu dari sebuah video walaupun hanya melalui caption-nya. Saya melihat pranala GitHub pada bagian footer untuk melihat teknologi dibalik aplikasi web tersebut.

    Ternyata aplikasi web tersebut tidak menggunakan teknologi yang benar-benar terkini. Tidak juga menggunakan kecerdasan buatan atau algoritma yang kompleks. Hanya JavaScript sederhana, beberapa paket NPM dan berkas caption. Hal yang sangat menarik bagi saya.

    Saya rasa kekurangan dari Search web.dev Live adalah ia hanya dapat mencari dari video acara web.dev Live. Omong-omong, video acara web.dev Live ditayangkan di YouTube. Dan berkas caption-nya disimpan di repositori GitHubnya sendiri.

    “Bagaimana jika kita dapat melakukan pencarian tersebut, tetapi untuk semua video di YouTube”, pikir saya. Berdasarkan asumsi saya, algoritmanya mungkin seperti berikut:

    • JavaScript mengambil kata kunci dari input.
    • Cari kata kunci tersebut dari semua berkas caption yang ada.
    • Soroti hasil pencariannya.

    Sebenarnya mudah bukan? Dan ternyata tidak membutuhkan waktu lama hingga saya menemukan bahwa kita dapat mendapatkan caption YouTube dari sebuah pranala tertentu.

    Pertama-tama, kita harus membuka sebuah video YouTube kemudian mengaktifkan fitur caption-nya.

    Aktivasi Xaption YouTube
    Aktivasi Xaption YouTube

    Setelah itu, YouTube akan mengakses sebuah API. Respon dari API tersebut adalah caption dari video yang kita tonton.

    Alat Pengembang Peramban Menampilkan Respon Caption YouTube
    Alat Pengembang Peramban Menampilkan Respon Caption YouTube

    Berikut contoh pranala API-nya:

    const const ccUrl: "https://www.youtube.com/api/timedtext?v=CWu29PRCUvQ&asr_langs=de%2Cen%2Ces%2Cfr%2Cit%2Cja%2Cko%2Cnl%2Cpt%2Cru&caps=asr&exp=xftt&xorp=true&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1612625982&sparams=ip%2Cipbits%2Cexpire%2Cv%2Casr_langs%2Ccaps%2Cexp%2Cxorp%2Cxoaf&signature=CC705091625358E3A3883C981A21BFC4F6E35569.77DAB826C17A839CEE703DB894D73D8AECCA0AE2&key=yt8&lang=en-GB&fmt=json3&xorb=2&xobt=3&xovt=3"ccUrl = 'https://www.youtube.com/api/timedtext?v=CWu29PRCUvQ&asr_langs=de%2Cen%2Ces%2Cfr%2Cit%2Cja%2Cko%2Cnl%2Cpt%2Cru&caps=asr&exp=xftt&xorp=true&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1612625982&sparams=ip%2Cipbits%2Cexpire%2Cv%2Casr_langs%2Ccaps%2Cexp%2Cxorp%2Cxoaf&signature=CC705091625358E3A3883C981A21BFC4F6E35569.77DAB826C17A839CEE703DB894D73D8AECCA0AE2&key=yt8&lang=en-GB&fmt=json3&xorb=2&xobt=3&xovt=3'

    Mari kita cermati parameter kueri yang terdapat pada pranala tersebut:

    {
      "v": "CWu29PRCUvQ",
      "asr_langs": "de,en,es,fr,it,ja,ko,nl,pt,ru",
      "caps": "asr",
      "exp": "xftt",
      "xorp": "true",
      "xoaf": "5",
      "hl": "en",
      "ip": "0.0.0.0",
      "ipbits": "0",
      "expire": "1612625982",
      "sparams": "ip,ipbits,expire,v,asr_langs,caps,exp,xorp,xoaf",
      "signature": "CC705091625358E3A3883C981A21BFC4F6E35569.77DAB826C17A839CEE703DB894D73D8AECCA0AE2",
      "key": "yt8",
      "lang": "en-GB",
      "fmt": "json3",
      "xorb": "2",
      "xobt": "3",
      "xovt": "3"
    }

    Kita dapat memahami beberapa pasang key-value:

    • v berisi identitas unik video YouTube.
    • hl kependekan dari host language. Cukup umum bagi layanan API Google menggunakan key ini. Fungsinya mendefinisikan sesuatu yang berhubungan dengan pengaturan bahasa.
    • expire kapan request API akan kadaluarsa. Nilainya berformat detik.
    • signature identitas acak dari setiap request. Menurut saya, nilai dari key ini mendefinisikan request mana yang berhak diproses. Tanggal kadaluarsa nampaknya juga memiliki koneksi dengan identitas ini. Saya mencoba menghilangkan parameter ini ketika mengakses API-nya tetapi malah mendapatkan respon gagal.
    • fmt mendefinisikan format respon. Secara default, respon API terformat sebagai json3. Anggapan saya nilai tersebut dapat diubah. Saya mencoba format caption lain, seperti vtt dan ternyata bekerja.

    Sebenarnya masih terdapat beberapa paasng key-value lainnya, tetapi yang saya sebutkan di atas merupakan yang paling penting.

    Alat Pengembang Menampilkan Respon Caption YouTube dalam Format WebVTT
    Alat Pengembang Menampilkan Respon Caption YouTube dalam Format WebVTT

    Yeaye! Saya rasa kita dapat menggunakan pranala API tersebut untuk membangun aplikasi web pencari kata kunci dari sebuah video YouTube.


    Tetapi sebelum kita melanjutkan pembahasan, mari kita melihat terlebih dahulu isi dari format WebVTT:

    WEBVTT
    Kind: captions
    Language: en
    
    00:00:00.357 --> 00:00:01.822
    - [Narrator] There is
    a growing feeling today
    
    00:00:01.822 --> 00:00:04.699
    that something is wrong with
    our system of education.

    WebVTT hanyalah sebuah teks biasa dengan format konten tertentu. Secara umum, WebVTT terdiri dari 2 bagian, header dan cue.

    WEBVTT
    Kind: captions
    Language: en

    Header berisi metadata mengenai caption, seperti bahasa yang digunakan atau pembuat caption-nya.

    00:00:00.357 --> 00:00:01.822
    - [Narrator] There is
    a growing feeling today
    
    00:00:01.822 --> 00:00:04.699
    that something is wrong with
    our system of education.

    Cue dapat dibagi menjadi 3 bagian, waktu mulai, waktu berakhir dan teks. Teks merupakan caption yang akan ditampilkan saat video diputar. Waktu mulai menunjukkan kapan suatu bagian teks akan ditampilkan sedangkan waktu berakhir menunjukkan kapan dihilangkan.


    Bahan Baku

    Berikut beberapa paket NPM utama yang dapat kita gunakan untuk membuat API:

    • @vercel/node, kita akan mengunggah kode kita ke Vercel, sehingga kita membutuhkan paket NPM ini untuk menjalankan API kita di sana.
    • chrome-aws-lambda, Vercel Serverless Function di balik layar berjalan di atas AWS Lambda. AWS Lambda memiliki batas pada seberapa besar ukuran fungsi yang dijalankan. Kalau tidak salah, ukuran maksimum sekitar 50MB dan ukuran maksimum ketika diekstrak (unzipped) sekitar 250MB. Untuk mengakomodasi batasan ini, kita tidak dapat menggunakan puppeteer yang biasa. Karena puppeteer yang biasa akan mengunduh berkas Chrome secara penuh. Sebagai gantinya, kita akan menggunakan puppeteer-core dan paket ini sebagai pendampingnya. Paket ini akan memasang versi chrome yang lebih kecil ukurannya karena terkompresi menggunakan algoritma Brotli.
    • flexsearch, kita akan membuat API untuk pencarian, sehingga kita membutuhkan mekanisme pencarian supaya efektif. Meskipun kita dapat menggunakan fungsi Array.prototype.filter bawaan JavaScript, menurut pendapat saya akan lebih baik jika kita dapat meningkatkan peforma pencarian menggunakan perpustakaan pihak ke-3. Aplikasi web Searh web.dev Live juga menggunakan paket NPM ini.
    • get-urls, paket NPM ini sangat berguna untuk mengekstrak metadata pranala dari sebuah string. Secara spesifik untuk mendapatkan pranala logo.
    • isomorphic-fetch, kita akan menggunakan paket ini untuk mengambil data caption dari REST API YouTube.
    • joi, paket NPM ini berguna untuk validasi objek. Dia akan memastikan semua data sesuai dengan format yang telah ditentukan.
    • pino-logflare, log merupakan bagian yang cukup krusial dalam teknologi. Kita dapat menggunakannya untuk mencari dan memperbaiki bug. Namun, Vercel memiliki aturan yang berbeda dalam hal penyimpanan log. Singkatnya, cukup susah untuk mendapatkan informasi penting dari log karena batasan tersebut. Untungnya, Vercel juga menyediakan fitur bernama Log Drain. Sederhananya, Vercel tidak akan menyimpan log di server-nya, melainkan pada layanan lain yang dikhususkan untuk menyimpan log aplikasi. Salah satunya adalah Logflare. Paket ini mengintegrasikan logger express bernama Pino dengan Logflare.
    • puppeteer-core, puppeteer merupakan headless chrome, kita membutuhkannya untuk membuka halaman YouTube.
    • string-strip-html, terkadang caption berisi kode HTML. Kita membutuhkan paket NPM ini untuk menghilangkannya.
    • caption, kita mendapatkan data caption dari YouTube API dalam format WebVTT. Paket NPM ini berguna untuk mem-parsing dan mengubah format tersebut menjadi objek JavaScript.
    • typescript, JavaScript dengan tipe anotasi.

    Untuk melihat lebih banyak paket NPM yang digunakan, teman-teman dapat melihatnya di berkas package.json pada repositori berikut, Cari Teks Video API.

    Modul

    Kita telah mempunyai semua bahan bakunya. Sekarang kita akan membuat API dengan 2 endpoint, yang pertama adalah / dan yang kedua adalah /search. Endpoint indeks hanya kita gunakan untuk mendapatkan data caption dari YouTube. Sedangkan endpoint search untuk pencarian. Pada artikel kali ini, kita akan lebih banyak membahas mengenai endpoint pencarian.

    Diagram API Caption YouTube
    Diagram API Caption YouTube

    Berdasarkan diagram di atas, kita dapat membagi REST API yang akan kita buat menjadi 8 modul yang lebih kecil.

    Guardian

    Sesuai dengan namanya, Guardian yang berarti penjaga. Fungsi dari modul ini adalah memastikan parameter dan data yang didapat sesuai. Kita akan menggunakan Joi sebagai Guardian kita.

    import import joijoi from 'joi'
    
    const const indexQuery: anyindexQuery = import joijoi.object({
      url: anyurl: import joijoi.string().required(),
      page: anypage: import joijoi.string().optional(),
      size: anysize: import joijoi.string().optional(),
      paginated: anypaginated: import joijoi.number().integer().allow(0, 1).optional()
    })
    
    const const searchQuery: anysearchQuery = const indexQuery: anyindexQuery.keys({
      q: anyq: import joijoi.string().min(3).required(),
      marked: anymarked: import joijoi.number().integer().allow(0, 1).optional()
    })

    Mengikuti endpoint REST API yang kita buat, ada 2 jenis parameter kueri. Yang pertama untuk indeks dan yang kedua untuk pencarian. Parameter kueri untuk pencarian meng-extend indeks.

    Jika teman-teman ingin mengetahui fungsi setiap parameter kueri, teman-teman dapat mempelajarinya di dokumentasi Cari Teks Video API - Request.

    Retriever

    Retriever berguna untuk mendapatkan data dari parameter kueri. Seperti yang kita lakukan pada Guardian, kita juga harus membagi retriever menjadi 2 jenis fungsi, indeks dan pencarian.

    Untuk url, kita memasukkan pranala YouTube secara langsung sebagai parameter kueri. Tetapi jika kita membuka halaman YouTube secara umum, akan membutuhkan waktu yang lebih lama. Sehingga kita menggunakan versi embedded-nya. Oleh karena itu kita harus mengubah pranala tersebut menjadi versi embedded-nya.

    Yang kita butuhkan hanyalah identitas video YouTube. Kita dapat menggunakan Regular Expression atau pada umumnya disingkat sebagai RegEx untuk menyelesaikan permasalahan tersebut. Berikut RegEx yang akan kita gunakan:

    const const youtubeUrl: "https://www.youtube.com/watch?v=okpg-lVWLbE"youtubeUrl = 'https://www.youtube.com/watch?v=okpg-lVWLbE'
    const const result: RegExpExecArray | nullresult = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi
      .RegExp.exec(string: string): RegExpExecArray | null
    Executes a search on a string using a regular expression pattern, and returns an array containing the results of that search.
    @paramstring The String object or string literal on which to perform the search.
    exec
    (const youtubeUrl: "https://www.youtube.com/watch?v=okpg-lVWLbE"youtubeUrl)
    let let id: stringid = '' if (var Array: ArrayConstructorArray.ArrayConstructor.isArray(arg: any): arg is any[]isArray(const result: RegExpExecArray | nullresult) && const result: RegExpExecArrayresult.Array<string>.length: number
    Gets or sets the length of the array. This is a number one higher than the highest index in the array.
    length
    > 0) {
    let id: stringid = const result: RegExpExecArrayresult[1] }

    RegEx yang cukup panjang tetapi bekerja dengan baik. Saya mendapatkannya dari utas StackOverflow berikut, Regular Expression untuk Identitas YouTube.

    Setelah kita mendapatkan identitas video tersebut, kita dapat menggabungkannya dengan pranala embedded YouTube.

    // previous code
    
    const const youtubeEmbedUrl: stringyoutubeEmbedUrl = `https://www.youtube.com/embed/${const id: stringid}?hl=en&cc_lang_pref=en&cc_load_policy=1&autoplay=1`

    Kita juga menambahkan beberapa parameter pada pranala tersebut.

    • hl, sama seperti hl yang terdapat pada API caption YouTube yang telah kita diskusikan di atas. hl merupakan kependekan dari host language yang berfungsi untuk mengatur bahasa antarmuka pemutar video.
    • cc_lang_pref, parameter ini mengatur bahasa default untuk menampilkan caption.
    • cc_load_policy, berfungsi untuk mengaktifkan caption secara otomatis jika nilainya 1.
    • autoplay, memainkan video secara otomatis ketika termuat jika nilainya 1.

    Fetcher

    Modul ini merupakan bagian yang memerlukan waktu terbanyak untuk dibuat. Fetcher berisi beberapa submodul untuk mendapatkan API caption YouTube, fetcher indeks dan fetcher pencarian.

    YouTube Closed Captions Fetcher

    Fetcher ini sebenarnya digunakan pada endpoint indeks. Tetapi untuk memahami bagaimana kita mendapatkan data caption, kita harus membahasnya.

    Kita telah memahami algoritma untuk mendapatkan pranala API caption YouTube. Kita harus mengubahnya menjadi kode yang nyata. Kita menggunakan puppeteer dan Chrome AWS Lambda untuk melakukannya. Mari kita lihat potongan kode berikut:

    import import chromechrome from 'chrome-aws-lambda'
    import import puppeteerpuppeteer from 'puppeteer-core'
    
    async function function getYoutubeCC(url: string): Promise<string>getYoutubeCC(url: stringurl: string) {
      const const browser: anybrowser = await import puppeteerpuppeteer.launch({
        args: anyargs: import chromechrome.args,
        executablePath: anyexecutablePath: await import chromechrome.executablePath,
        headless: anyheadless: import chromechrome.headless
      })
    
      const const page: anypage = await const browser: anybrowser.newPage()
    
      await const page: anypage.setRequestInterception(true)
    
      let let ccUrl: stringccUrl = ''
    
      const page: anypage.on('request', (request: anyrequest) => {
        if (request: anyrequest.resourceType() === 'xhr') {
          const const _ccUrl: any_ccUrl = request: anyrequest.url()
          if (const _ccUrl: any_ccUrl.includes('https://www.youtube.com/api/timedtext')) {
            let ccUrl: stringccUrl = const _ccUrl: any_ccUrl.replace('json3', 'vtt')
          }
        }
        request: anyrequest.continue()
      })
    
      await const page: anypage.goto(url: stringurl, {
        waitUntil: stringwaitUntil: 'networkidle0'
      })
    
      return let ccUrl: stringccUrl
    }
    
    (async () => {
      const const ccUrl: stringccUrl = await function getYoutubeCC(url: string): Promise<string>getYoutubeCC(const youtubeEmbedUrl: stringyoutubeEmbedUrl)
    })()

    Kita membuat instance peramban (browser) dan memasukkan opsi pengaturan untuk menggunakan Chrome AWS Lambda. Selain itu, kita juga menginisiasi halaman kosong baru.

    Untuk mendapatkan pranala API caption YouTube, kita harus meng-intercept request peramban. Kita dapat melakukannya dengan memanggil setRequestInterception yang terdapat pada objek page dengan parameter true. Sekarang kita dapat membuat event listener untuk setiap event request. Setelah itu, kita harus menyaring request tersebut berdasarkan tipenya. Dalam hal ini kita harus mendapatkan request bertipe xhr.

    Selanjutnya kita juga harus memerika apakah request bertipe xhr tersebut merupakan pranala API caption YouTube atau bukan. Jika ditemukan, maka kita harus mengubah format json3 menjadi vtt. Event callback ini dipanggil setiap peramban melakukan request. Sehingga kita harus memanggil fungsi continue secara manual untuk memanggil event request berikutnya.

    Bagian terakhir dari bagian ini adalah memanggil fungsi goto dengan pranala embedded YouTube yang telah kita deklarasikan pada bagian sebelumnya sebagai parameter pertama. Kita juga menggunakan opsi waitUntil dengan nilai networkidle0. Opsi tersebut mendefinisikan bahwa kita akan menunggu hingga halaman selesai termuat secara menyeluruh.

    Index Fetcher

    Setelah kita mendapatkan pranala caption dari YouTube Closed Captions Fetcher, kita dapat mengambil data WebVTT-nya.

    (async () => {
      // previous code
    
      const const data: Vtt[]data: 
    type Vtt = {
        id?: number;
        start: number;
        end: number;
        text: string;
    }
    Vtt
    [] = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
    [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
    fetch
    (const ccUrl: stringccUrl)
    .Promise<Response>.then<string, never>(onfulfilled?: ((value: Response) => string | PromiseLike<string>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<string>
    Attaches callbacks for the resolution and/or rejection of the Promise.
    @paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
    then
    ((ccResponse: ResponseccResponse) => {
    if (ccResponse: ResponseccResponse.Response.ok: boolean
    The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok)
    ok
    ) {
    return ccResponse: ResponseccResponse.Body.text(): Promise<string>
    [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text)
    text
    ()
    } return '' }) .Promise<string>.then<Vtt[], Vtt[]>(onfulfilled?: ((value: string) => Vtt[] | PromiseLike<Vtt[]>) | null | undefined, onrejected?: ((reason: any) => Vtt[] | PromiseLike<Vtt[]>) | null | undefined): Promise<Vtt[]>
    Attaches callbacks for the resolution and/or rejection of the Promise.
    @paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
    then
    ((ccText: stringccText) => {
    const const ccStriped: stringccStriped = function stripHtml(string: string): stringstripHtml(ccText: stringccText) const const ccJson: CCNodeListccJson = function vttToJson(vtt: string): CCNodeListvttToJson(const ccStriped: stringccStriped) const const ccData: Vtt[]ccData = const ccJson: CCNodeListccJson .Array<CCNode>.filter(predicate: (value: CCNode, index: number, array: CCNode[]) => unknown, thisArg?: any): CCNode[] (+1 overload)
    Returns the elements of an array that meet the condition specified in a callback function.
    @parampredicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
    filter
    ((item: CCNodeitem) => {
    return item: CCNodeitem.type: "cue"type === 'cue' }) .
    Array<CCNode>.map<{
        start: number;
        end: number;
        text: string;
    }>(callbackfn: (value: CCNode, index: number, array: CCNode[]) => {
        start: number;
        end: number;
        text: string;
    }, thisArg?: any): {
        start: number;
        end: number;
        text: string;
    }[]
    Calls a defined callback function on each element of an array, and returns an array that contains the results.
    @paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
    map
    ((item: CCNodeitem) => {
    return { start: numberstart: function toSecond(timestamp: number): numbertoSecond((item: CCNodeitem as
    type CCNode = {
        type: "cue";
        data: {
            start: number;
            end: number;
            text: string;
            settings?: string;
        };
    }
    CCNode
    ).
    data: {
        start: number;
        end: number;
        text: string;
        settings?: string;
    }
    data
    .start: numberstart || 0),
    end: numberend: function toSecond(timestamp: number): numbertoSecond((item: CCNodeitem as
    type CCNode = {
        type: "cue";
        data: {
            start: number;
            end: number;
            text: string;
            settings?: string;
        };
    }
    CCNode
    ).
    data: {
        start: number;
        end: number;
        text: string;
        settings?: string;
    }
    data
    .end: numberend || 0),
    text: stringtext: function stripWhitespaceNewLine(string: string): stringstripWhitespaceNewLine((item: CCNodeitem as
    type CCNode = {
        type: "cue";
        data: {
            start: number;
            end: number;
            text: string;
            settings?: string;
        };
    }
    CCNode
    ).
    data: {
        start: number;
        end: number;
        text: string;
        settings?: string;
    }
    data
    .text: stringtext)
    } }) as
    type Vtt = {
        id?: number;
        start: number;
        end: number;
        text: string;
    }
    Vtt
    []
    return const ccData: Vtt[]ccData }) })()

    Catatan: CCNode merupakan deklarasi tipe data yang saya buat sendiri untuk tujuan demo. Deklarasi tipe data asli yang berasal dari paket NPM subtitle adalah NodeCue.

    Kita menggunakan fungsi fetch untuk mendapatkan datanya. Jika responnya sukses, kita dapat memanggil fungsi text dan mendapatkan data caption dalam bentuk teks. Jika responsennya gagal, kita hanya mengembalikan nilai string kosong.

    Biasanya, data caption berisi kode HTML. Jadi kita harus menghilangkannya dengan memanggil fungsi stripHtml. Fungsi tersebut mengembalikan nilai dalam bentuk string juga. Untuk memudahkan kita berinteraksi dengan data tersebut, kita harus mengubahnya menjadi bentuk yang umum di JavaScript.

    Kita mengubahnya menjadi objek JSON dengan cara memanggil fungsi vttToJson. Di balik layar, fungsi tersebut menggunakan fungsi parseSync dari paket NPM subtitle. Hasil nilai kembaliannya berupa array of object.

    Terkadang item dari array tersebut tercampur antara header dan cue. Item yang kita butuhkan hanyalah cue, jadi kita harus memfilternya. Setelah itu, kita memetakan array tersebut menjadi bentuk objek yang kita butuhkan, waktu mulai, waktu berakhir dan teks caption.

    Waktu mulai dan waktu berakhir terbaca dalam format mili detik tetapi skema pranala YouTube pada umumnya menggunakan waktu dalam format detik. Oleh karena itu kita harus mengubahnya menjadi format yang benar menggunakan fungsi utilitas toSecond. Fungsi tersebut hanyalah fungsi sederhana yang membulatkan pembagian nilai mili detik dengan 1000.

    Terkadang properti teks berisi spasi dan garis baru di akhir kalimat. Kita dapat membersihkannya menggunakan fungsi utilitas stripWhitespaceNewLine.

    Search Fetcher

    Fetcher ini digunakan pada endpoint pencarian. Hal yang dilakukan adalah mendapatkan respon JSON dari endpoint indeks.

    (async () => {
      // previous code
    
      const const indexEndpoint: stringindexEndpoint = `https://cari-teks-video-api.vercel.app/api/?url=${const youtubeUrl: stringyoutubeUrl}&paginated=0`
    
      const const list: FetcherReturnTypelist: 
    type FetcherReturnType = {
        data: Vtt[];
    }
    FetcherReturnType
    = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
    [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
    fetch
    (const indexEndpoint: stringindexEndpoint)
    .Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>
    Attaches callbacks for the resolution and/or rejection of the Promise.
    @paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
    then
    ((response: Responseresponse) => {
    if (response: Responseresponse.Response.ok: boolean
    The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok)
    ok
    ) {
    return response: Responseresponse.Body.json(): Promise<any>
    [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)
    json
    ()
    } return [] }) .
    Promise<any>.then<{
        data: Vtt[];
    }, FetcherReturnType>(onfulfilled?: ((value: any) => {
        data: Vtt[];
    } | PromiseLike<{
        data: Vtt[];
    }>) | null | undefined, onrejected?: ((reason: any) => FetcherReturnType | PromiseLike<FetcherReturnType>) | null | undefined): Promise<FetcherReturnType | {
        data: Vtt[];
    }>
    Attaches callbacks for the resolution and/or rejection of the Promise.
    @paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
    then
    ((data: Vtt[]data:
    type Vtt = {
        id?: number;
        start: number;
        end: number;
        text: string;
    }
    Vtt
    []) => {
    return { data: Vtt[]data } }) })()

    Secara default, ketika kita melakukan request ke endpoint indeks, responnya akan terpaginasi. Jadi kita harus menambahkan parameter paginated=0 untuk mendapatkan hasil keseluruhan.

    ID Generator

    Flexsearch membutuhkan setiap item memiliki identitas unik. Modul ini akan meng-generate identitas tersebut.

    (async () => {
      // previous code
    
      const 
    const generatedIdList: {
        id: number;
        start: number;
        end: number;
        text: string;
    }[]
    generatedIdList
    = const list: Vtt[]list.
    Array<Vtt>.map<{
        id: number;
        start: number;
        end: number;
        text: string;
    }>(callbackfn: (value: Vtt, index: number, array: Vtt[]) => {
        id: number;
        start: number;
        end: number;
        text: string;
    }, thisArg?: any): {
        id: number;
        start: number;
        end: number;
        text: string;
    }[]
    Calls a defined callback function on each element of an array, and returns an array that contains the results.
    @paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
    map
    ((item: Vttitem, id: numberid) => {
    return { ...item: Vttitem, id: numberid } }) })()

    Modul ini akan memetakan objek sebelumnya dan menambahkan properti baru untuk identitas. Nilai dari identitas tersebut dapat kita ambil dari indeks array setiap item.

    Finder

    Kita menggunakan paket NPM bernama Flexsearch untuk melakukan pencarian kata kunci. Berdasarkan uji coba yang disebutkan pada halaman GitHubnya, Flexsearch merupakan perpustakaan pencarian teks menyeluruh yang tercepat dan paling efisien penggunaan memorinya. Selain itu Flexsearch juga tanpa dependensi apapun.

    import import FlexsearchFlexsearch from 'flexsearch'
    
    (async () => {
      // previous code
    
      const const index: anyindex = import FlexsearchFlexsearch.create<
    type Vtt = {
        id?: number;
        start: number;
        end: number;
        text: string;
    }
    Vtt
    >({
    doc: {
        id: string;
        field: string[];
    }
    doc
    : {
    id: stringid: 'id', field: string[]field: ['text'] } }) const generatedIdList: Vtt[]generatedIdList.Array<Vtt>.forEach(callbackfn: (value: Vtt, index: number, array: Vtt[]) => void, thisArg?: any): void
    Performs the specified action for each element in an array.
    @paramcallbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
    forEach
    ((item: Vttitem) => {
    const index: anyindex.add(item: Vttitem) }) const const keyword: "education"keyword = 'education' const const resultList: anyresultList = await const index: anyindex.search(const keyword: "education"keyword) })()

    Pada mulanya kita membuat indeks dengan cara memanggil fungsi create. Kita memasukkan beberapa konfigurasi seperti nama key dari identitas dan nama key apa yang Flexsearch harus cari ketika melakukan pencarian.

    Setelah itu, kita lakukan perulangan dan menambahkan setiap item ke dalam indeks dengan cara memanggil fungsi add. Jika kita ingin mencari kata kunci, yang perlu dilakukan hanyalah memanggil fungsi search dari objek index dengan kata kunci yang dimaksud sebagai parameter pertama. Nilai yang dihasilkan dari pemanggilan tersebut berupa array yang telah difilter sesuai dengan kata kunci.

    Highlighter

    Sayangnya, Flexsearch belum memiliki highlighter bawaan ataupun yang dapat kita ubah-ubah sendiri. Namun, kita dapat mengimplementasinya sendiri. Karena nilai yang dikembalikan dari fungsi search merupakan daftar yang telah difilter, kita dapat melakukan perulangan dan meng-highlight kata kunci yang ditemukan dengan tag HTML.

    (async () => {
      // previous code
    
      function function (local function) replacer(match: string): stringreplacer(match: stringmatch: string) {
        return `<mark class="cvt-highlight">${match: stringmatch}</mark>`
      }
    
      const const highlightedList: Vtt[]highlightedList: 
    type Vtt = {
        id?: number;
        start: number;
        end: number;
        text: string;
    }
    Vtt
    [] = const resultList: Vtt[]resultList.
    Array<Vtt>.map<{
        text: string;
        id?: number;
        start: number;
        end: number;
    }>(callbackfn: (value: Vtt, index: number, array: Vtt[]) => {
        text: string;
        id?: number;
        start: number;
        end: number;
    }, thisArg?: any): {
        text: string;
        id?: number;
        start: number;
        end: number;
    }[]
    Calls a defined callback function on each element of an array, and returns an array that contains the results.
    @paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
    map
    ((item: Vttitem) => {
    let let text: stringtext = '' if (item: Vttitem.text: stringtext.String.includes(searchString: string, position?: number): boolean
    Returns true if searchString appears as a substring of the result of converting this object to a String, at one or more positions that are greater than or equal to position; otherwise, returns false.
    @paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String.
    includes
    (const keyword: stringkeyword)) {
    let text: stringtext = `${item: Vttitem.text: stringtext}`.
    String.replace(searchValue: {
        [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string;
    }, replacer: (substring: string, ...args: any[]) => string): string (+3 overloads)
    Replaces text in a string, using an object that supports replacement within a string.
    @paramsearchValue A object can search for and replace matches within a string.@paramreplacer A function that returns the replacement text.
    replace
    (new
    var RegExp: RegExpConstructor
    new (pattern: RegExp | string, flags?: string) => RegExp (+2 overloads)
    RegExp
    (`${const keyword: stringkeyword}`, 'gi'), function (local function) replacer(match: string): stringreplacer)
    } else { const const multipleWords: RegExpMatchArray | nullmultipleWords = const keyword: stringkeyword.
    String.match(matcher: {
        [Symbol.match](string: string): RegExpMatchArray | null;
    }): RegExpMatchArray | null (+1 overload)
    Matches a string or an object that supports being matched against, and returns an array containing the results of that search, or null if no matches are found.
    @parammatcher An object that supports being matched against.
    match
    (/\S+/g)
    if (const multipleWords: RegExpMatchArray | nullmultipleWords) { let text: stringtext = `${item: Vttitem.text: stringtext}`.
    String.replace(searchValue: {
        [Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string;
    }, replacer: (substring: string, ...args: any[]) => string): string (+3 overloads)
    Replaces text in a string, using an object that supports replacement within a string.
    @paramsearchValue A object can search for and replace matches within a string.@paramreplacer A function that returns the replacement text.
    replace
    (
    new
    var RegExp: RegExpConstructor
    new (pattern: RegExp | string, flags?: string) => RegExp (+2 overloads)
    RegExp
    (`${const multipleWords: RegExpMatchArraymultipleWords.Array<string>.join(separator?: string): string
    Adds all the elements of an array into a string, separated by the specified separator string.
    @paramseparator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.
    join
    ('|')}`, 'gi'),
    function (local function) replacer(match: string): stringreplacer ) } } return { ...item: Vttitem, text: stringtext } }) })()

    Jika kata kunci yang digunakan hanya terdiri dari satu kata, kita menggunakan fungsi String.prototype.includes untuk mengecek apakah teks berisi kata kunci yang dimaksud. Jika ditemukan, maka kita dapat melakukan mekanisme highlighting.

    Sedangkan jika kata kunci yang digunakan terdiri dari beberapa kata, kita tidak dapat menggunakan metode tersebut. Sebagai gantinya, kita menggabungkan setiap kata pada kata kunci tersebut menjadi format RegEx. Katakanlah kata kunci yang digunakan adalah current education, kita mengubahnya menjadi /current|education/gi. Sehingga ketika melakukan mekanisme highlighting, setiap kata akan memiliki pembungkusnya masing-masing.

    Paginator

    Modul ini mengubah array menjadi versi terpaginasinya. Saya menemukan jawaban ringkas dari StackOverflow yang dapat membantu kita menyelesaikan permasalahan ini, Mempaginasi Array JavaScript.

    (async () => {
      // previous code
    
      const const pageNumber: 1pageNumber = 1
      const const pageSize: 10pageSize = 10
    
      const const paginatedList: Vtt[]paginatedList = const highlightedList: Vtt[]highlightedList
        .Array<Vtt>.slice(start?: number, end?: number): Vtt[]
    Returns a copy of a section of an array. For both start and end, a negative index can be used to indicate an offset from the end of the array. For example, -2 refers to the second to last element of the array.
    @paramstart The beginning index of the specified portion of the array. If start is undefined, then the slice begins at index 0.@paramend The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. If end is undefined, then the slice extends to the end of the array.
    slice
    ((const pageNumber: 1pageNumber - 1) * const pageSize: 10pageSize, const pageNumber: 1pageNumber * const pageSize: 10pageSize)
    })()

    Satu-satunya parameter yang kita butuhkan adalah nomor halaman dan banyaknya item. Nomor halaman adalah halaman saat ini yang akan dikembalikan dan banyaknya item adalah berapa banyak item yang akan dikembalikan.

    ID Remover

    Identitas yang ditambahkan oleh modul ID Generator hanya digunakan untuk membuat indeks untuk pencarian. Di sini kita sudah tidak membutuhkannya dan dapat dihapus.

    (async () => {
      // previous code
    
      const const removedIdList: Vtt[]removedIdList = const paginatedList: Vtt[]paginatedList.Array<Vtt>.map<Vtt>(callbackfn: (value: Vtt, index: number, array: Vtt[]) => Vtt, thisArg?: any): Vtt[]
    Calls a defined callback function on each element of an array, and returns an array that contains the results.
    @paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
    map
    ((item: Vttitem) => {
    delete item: Vttitem.id?: number | undefinedid return item: Vttitem }) })()

    Untuk menghapus identitas tersebut, kita dapat melakukan pemetaan pada array menjadi array yang baru dan menghapus properti id.

    Formatter

    Modul terakhir adalah formatter. Kita tidak akan membahas terlalu panjang bagaimana modul ini terimplementasi. Tetapi secara garis besar modul ini akan menghasilkan pranala untuk halaman sebelumnya, selanjutnya, pertama dan terakhir.

    (async () => {
      // previous code
    
      const const options: FormatterOptionsoptions: 
    type FormatterOptions = {
        reqUrl: string;
        page: number;
        size: number;
        dataLength: number;
    }
    FormatterOptions
    = {
    reqUrl: stringreqUrl: 'https://cari-teks-video-api.vercel.app/api', page: numberpage: const pageNumber: numberpageNumber, size: numbersize: const pageSize: numberpageSize, dataLength: numberdataLength: const removedIdList: Vtt[]removedIdList.Array<Vtt>.length: number
    Gets or sets the length of the array. This is a number one higher than the highest index in the array.
    length
    } const const response: FormatterReturnTyperesponse:
    type FormatterReturnType = {
        first: string;
        last: string;
        prev: string;
        next: string;
        total: number;
        page: number;
        data: Vtt[];
    }
    FormatterReturnType
    = {
    first: stringfirst: function getPaginationUrl(type: PaginationUrlType, options: FormatterOptions): stringgetPaginationUrl(enum PaginationUrlTypePaginationUrlType.function (enum member) PaginationUrlType.FirstFirst, const options: FormatterOptionsoptions), last: stringlast: function getPaginationUrl(type: PaginationUrlType, options: FormatterOptions): stringgetPaginationUrl(enum PaginationUrlTypePaginationUrlType.function (enum member) PaginationUrlType.LastLast, const options: FormatterOptionsoptions), prev: stringprev: function getPaginationUrl(type: PaginationUrlType, options: FormatterOptions): stringgetPaginationUrl(enum PaginationUrlTypePaginationUrlType.function (enum member) PaginationUrlType.PrevPrev, const options: FormatterOptionsoptions), next: stringnext: function getPaginationUrl(type: PaginationUrlType, options: FormatterOptions): stringgetPaginationUrl(enum PaginationUrlTypePaginationUrlType.function (enum member) PaginationUrlType.NextNext, const options: FormatterOptionsoptions), total: numbertotal: const options: FormatterOptionsoptions.dataLength: numberdataLength, page: numberpage: const options: FormatterOptionsoptions.page: numberpage, data: Vtt[]data: const removedIdList: Vtt[]removedIdList } })()

    Fungsi getPaginationRul menerima 2 parameter, parameter pertama berupa enum TypeScript. Enum tersebut berfungsi untuk mengidentifikasi paginasi yang mana yang sedang berlangsung. Parameter kedua adalah opsi yang terdiri dari:

    • reqUrl, request pranala saat ini. Jika saat ini request ke endpoint indeks maka pranalanya berakhiran /. Dan jika request ke endpoint pencarian, maka pranalanya berakhiran /search.
    • page, halaman pencarian saat ini, secara default nilai dari opsi ini adalah halaman pertama.
    • size, berapa banyak hasil yang harus dikembalikan oleh API, secara default nilai dari opsi ini adalah 10 item.
    • dataLength, berapa banyak data sebelum terpaginasi.

    Artikel ini hanya membahas bagian penting dari keseluruhan kode sumber. Jika teman-teman ingin mengetahui bagaimana penerapan nyatanya, teman-teman dapat mempelajarinya di repositori GitHub berikut, Cari Teks Video API.

    Penyangkalan

    1. “Metode” mengarah ke langkah-langkah atau penjelasan yang saya tulis pada artikel ini.
    2. “API” mengarah ke semua API yang tersedia pada https://cari-teks-video-api.vercel.app/api.
    3. Semua metode ini murni bertujuan untuk riset dan eksperimen.
    4. Jika YouTube memutuskan untuk mengubah skema API caption atau menggunakan cara lain, metode ini kemungkinan tidak dapat digunakan.
    5. Karena API yang dibuat di-host pada versi gratis dari Vercel yang memiliki beberapa batasan. Salah satunya adalah berapa banyak pemanggilan fungsi. Jika API tersebut tiba-tiba tidak dapat diakses, kemungkinan telah melewati batasan tersebut.
    6. Karena poin nomor 4 dan 5, saya tidak dapat menjamin API tersebut akan dapat digunakan secara terus-menerus. Tetapi saya akan melakukan pengecekan secara reguler untuk memastikannya tetap berjalan baik.
    7. Jangan gunakan untuk production. Saya tidak akan bertanggung jawab terhadap dampak apapun yang ditimbulkan dari penggunaan tersebut.
    8. YouTube memiliki API Data YouTube untuk Caption untuk menyediakan cara resmi mengakses caption. Silahkan gunakan layanan ini jika teman-teman ingin menggunakan fitur serupa untuk production.
    9. Jika teman-teman menemukan bug, silahkan mengirimkan issue di repositori GitHub berikut, Cari Teks Video API.

    Referensi




    Komentar