diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ac4ba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +sample/** +**.csv \ No newline at end of file diff --git a/code/conda_env.yml b/code/conda_env.yml deleted file mode 100644 index bdd51a6..0000000 --- a/code/conda_env.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: wikirl-gym -channels: -- pytorch -dependencies: -- python=3.8.5 -- anaconda -- cudatoolkit=10. -- numpy -- pip -- pip: - - gym==0.18.3 - - mujoco-py==2.0.2.13 - - numpy==1.20.3 - - torch==1.8.1 - - transformers==4.12.5 - - wandb==0.9.1 diff --git a/code/data/big_crawler.py b/code/data/big_crawler.py new file mode 100644 index 0000000..e70b978 --- /dev/null +++ b/code/data/big_crawler.py @@ -0,0 +1,144 @@ +import asyncio +import socket +import copy +import json +import os + +from aiohttp import ClientSession, TCPConnector, DummyCookieJar + +from youtube_helpers import BASE, ENDPOINT, PAYLOAD, USER_AGENT, parse_response + + + +# windows specific fix +if os.name == 'nt': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +# locks for concurrency +processing_lock = asyncio.Lock() +print_lock = asyncio.Lock() + +# data files +video_file = open('videos.txt', 'a') +channel_file = open('channels.txt', 'a') +playlist_file = open('playlists.jsonl', 'a', encoding = 'utf-8') + +# channels seen so far (continue from previous runs) +channels = set() +with open('channels.txt', 'r') as f: + for line in f: + channels.add(line.strip()) + + +async def get_recommendations( + video_id, session, unexplored_videos, + channel_set = channels, lock = processing_lock, + video_file = video_file, channel_file = channel_file, + playlist_file = playlist_file + ): + data = copy.deepcopy(PAYLOAD) + data['videoId'] = video_id + async with session.post(ENDPOINT, headers = {'User-Agent': USER_AGENT}, + json = data, timeout = 5) as response: + if response.ok is False: + return response.ok + + recommendations = parse_response(await response.json()) + for recommendation in recommendations: + async with lock: + video_file.write(recommendation['id'] + '\n') + + if recommendation['isPlaylist']: + playlist_file.write(json.dumps(recommendation) + '\n') + + if (recommendation['channel']['link'] is not None and + recommendation['channel']['link'] not in channel_set): + + channel_set.add(recommendation['channel']['link']) + channel_file.write(recommendation['channel']['link'] + '\n') + + if ('shorts' not in recommendation['link'] and + recommendation['isPlaylist'] is not True): + unexplored_videos.append(recommendation['id']) + + +async def worker(unexplored_videos, num_reqs, channels_set = channels): + async with print_lock: + print('worker started') + + while True: + # use ipv6 (helps with blocks) and leave concurrency to parallel connections + conn = TCPConnector(limit = 1, family = socket.AF_INET6, force_close = True) + async with ClientSession( + base_url = BASE, connector = conn, cookie_jar = DummyCookieJar() + ) as session: + for _ in range(num_reqs): + if len(unexplored_videos) == 0: + async with print_lock: + print('no more videos to explore, stopping worker') + return + + video_id = unexplored_videos.pop() + try: + ok_response = await get_recommendations(video_id, session, unexplored_videos) + if ok_response is False: + async with print_lock: + print("bad response, stopping worker (try restarting)") + return + except Exception as e: + async with print_lock: + print(e, video_id) + async with print_lock: + print( + 'finished connection, number of channels:', + len(channels_set), end = "\t\t\t\r" + ) + + +async def main(num_workers, num_reqs): + # read last num_workers * 1000 in to start the crawler back up + initial_videos = os.popen( + 'tail -n ' + str(num_workers * 1000) + ' videos.txt' + ).read().split('\n')[:-1] + + # if videos.txt doesn't have enough videos (cold start), fill it with some recommendations + if len(initial_videos) == 0: + assert len(channels) == 0, \ + 'channels.txt should be empty for cold start, delete channels.txt and try again' + + # start with an old and popular video + initial_videos = ['dQw4w9WgXcQ'] + async with ClientSession(base_url = BASE) as session: + # collect num_workers * num_reqs videos (just a heuristic) + while len(initial_videos) < num_workers * num_reqs: + video_id = initial_videos.pop() + await get_recommendations(video_id, session, initial_videos) + print( + f'collecting initial videos: {len(initial_videos)}/{num_workers * num_reqs}', + end = '\t\t\t\r' + ) + print('\nfinished collecting initial videos, starting asyncronous workers') + else: + print('loaded previous videos.txt and channels.txt, starting asyncronous workers') + + # split unexplored videos equally among workers + await asyncio.gather(*[ + worker(copy.deepcopy(initial_videos[i::num_workers]), num_reqs) + for i in range(num_workers) + ]) + + +try: + # launch 20 concurrent workers that each make 20 requests before restarting connection + # this would be the high end of normal individual youtube traffic + asyncio.run(main(num_workers = 20, num_reqs = 20)) +except (KeyboardInterrupt, Exception) as e: + print("\nfinal exception:", e) + + # make sure to exit cleanly + video_file.flush() + channel_file.flush() + playlist_file.flush() + video_file.close() + channel_file.close() + playlist_file.close() \ No newline at end of file diff --git a/code/data/channel_collector.py b/code/data/channel_collector.py new file mode 100644 index 0000000..a0d5cad --- /dev/null +++ b/code/data/channel_collector.py @@ -0,0 +1,305 @@ +import asyncio +import socket +import copy +import random +import csv +import os +import traceback + + +import pandas as pd +from aiohttp import ClientSession, TCPConnector, DummyCookieJar + + +from youtube_helpers import ( + BASE, + BROWSE_ENDPOINT, + PAYLOAD, + HEADER, + get_channel_id, + get_tab, + get_continuation, + tab_helpers, +) + + + +####################### GLOBAL VARIABLES ############################ +channel_lock = asyncio.Lock() + +error_lock = asyncio.Lock() +error_file = open('collection_errors.txt', 'a', encoding = "utf-8") + +completion_lock = asyncio.Lock() +completion_file = open('fully_collected.txt', 'a', encoding = "utf-8") + +request_lock = asyncio.Lock() +request_count = 0 +##################################################################### + + + +class Extractor(): + def __init__(self, name, filename, param, parse_func, features): + self.name = name + self.browse_param = param + self.parse_func = parse_func + self.features = features + + self.file_number = 1 + self.filename = filename + self.open_new_file() + + os.makedirs(self.filename, exist_ok = True) + + self.lock = asyncio.Lock() + + def open_new_file(self): + # Close the current file if it's open + if hasattr(self, 'file'): + self.file.close() + + # Open a new file with the current number + self.file = open(f'{self.filename}.csv', 'a', encoding="utf-8") + self.writer = csv.writer(self.file) + + # Write header if it doesn't exist + if os.path.getsize(f'{self.filename}.csv') == 0: + self.writer.writerow(self.features) + + # Increment the file number for next time + self.file_number += 1 + + def convert_to_parquet_if_large(self, threshold_gb = 1, tolerance = 0.2, force = False): + # Get the size of the file in GB + file_size_gb = os.path.getsize(f'{self.filename}.csv') / (2**30) + + # Check if the file size is within 20% of the threshold + if threshold_gb * (1 - tolerance) <= file_size_gb or force: + # Convert the CSV file to Parquet + df = pd.read_csv(f'{self.filename}.csv') + + # put file in a directory + df.to_parquet(os.path.join(self.filename, f'{self.filename}_{self.file_number - 1}.parquet')) + + # Delete the CSV file + self.file.flush() + self.file.close() + os.remove(f'{self.filename}.csv') + + # Open a new file for next time + self.open_new_file() + + + async def extract(self, session, channel_id, channel_link): + global request_count + + # initial request to get load a "selected tab" (e.g. videos, playlists, etc.) + payload = copy.deepcopy(PAYLOAD) + payload['browseId'] = channel_id + payload['params'] = self.browse_param + async with session.post( + BROWSE_ENDPOINT, headers = HEADER, + json = payload, timeout = 10 + ) as response: + async with request_lock: + request_count += 1 + + if response.ok is False: + async with error_lock: + print('bad response: ', response.status, response.reason) + raise BadResponseException() + + data = await response.json() + tab = get_tab(data, self.name) + + # tab may not exist (e.g. if channel has no playlists) + if tab is None: + return + + rows, token = self.parse_func( + channel_link, channel_id = channel_id, + tab = tab, response = data + ) + + if rows is not None: + async with self.lock: + self.writer.writerows(rows) + + # continue through continuation if it exists + while token is not None: + payload = copy.deepcopy(PAYLOAD) + payload['continuation'] = token + async with session.post( + BROWSE_ENDPOINT, headers = HEADER, + json = payload, timeout = 10 + ) as response: + async with request_lock: + request_count += 1 + + if response.ok is False: + async with error_lock: + print('bad response: ', response.status, response.reason) + raise BadResponseException() + + data = await response.json() + continuation = get_continuation(data) + + if continuation is None: + return + + rows, token = self.parse_func(channel_link, continuation = continuation) + + if rows is not None: + async with self.lock: + self.writer.writerows(rows) + + +class BadResponseException(Exception): + pass + + +########################## FUNCTION DECLARATIONS ########################### +async def get_channel(channel_link, session, error_lock = error_lock): + global request_count + + async with session.get( + channel_link, headers = HEADER, + allow_redirects = False, timeout = 10 + ) as response: + async with request_lock: + request_count += 1 + + # ignore 404 and just continue + if response.status == 404 or response.status == 303: + return None + + if response.ok is False: + async with error_lock: + print('bad response: ', response.status, response.reason) + raise BadResponseException() + + return get_channel_id(await response.text()) + + +async def worker(channels_left, session, extractors, + channel_lock = channel_lock, error_lock = error_lock): + while True: + async with channel_lock: + if len(channels_left) == 0: + break + channel_link = channels_left.pop() + + try: + # get request to get channel id (and for realism) + channel_id = await get_channel(channel_link, session) + if channel_id is None: + # 404 or 303 and thus ignore + # parsing errors will raise an assert instead + continue + + # collect all data from channel using extractors + for extractor in extractors: + await extractor.extract(session, channel_id, channel_link) + + # write to completion file + async with completion_lock: + completion_file.write(channel_link + '\n') + except BadResponseException: + async with error_lock: + print("Stopping worker due to bad response (try restarting)") + break + except asyncio.exceptions.TimeoutError: + # retry the channel + async with channel_lock: + channels_left.append(channel_link) + async with error_lock: + print('Caught a timeout') + except Exception: + async with error_lock: + print(f'Exception caught for {channel_link} and written to error file') + async with error_lock: + error_file.write(f'Exception caught for {channel_link}\n') + error_file.write(traceback.format_exc()) + error_file.flush() + + +async def launch_workers(channels, extractors, num_workers = 1, block_size = 100): + # use a singular session to benefit from connection pooling + # use ipv6 (helps with blocks) + while len(channels) > 0: + # get a block of channels + block = [channels.pop() for _ in range(min(block_size, len(channels)))] + conn = TCPConnector( + limit = None, family = socket.AF_INET6, force_close = True, ttl_dns_cache = 300 + ) + async with ClientSession( + base_url = BASE, connector = conn, cookie_jar = DummyCookieJar() + ) as session: + # start the workers + await asyncio.gather(*[ + worker(block, session, extractors) for _ in range(num_workers) + ]) + # connection will close here + + print(f'finished block of channels, {len(channels)} left, num requests made: {request_count}', end = '\r') + + # if len(channels) > 0: + # # force compress extractors + for extractor in extractors: + extractor.convert_to_parquet_if_large() + + +async def main(num_workers, block_size): + # extractors will be used to collect data from different tabs of a channel + extractors = [Extractor(**tab) for tab in tab_helpers] + + # read in shuffled channels + channels = set() + with open('shuffled_channels.txt', 'r', encoding = "ISO-8859-1") as f: + for line in f: + channels.add(line.strip()) + + # continue from previous run if possible + collected = [] + with open('fully_collected.txt', 'r', encoding = 'utf-8') as f: + for line in f: + collected.append(line.strip()) + collected_set = set(collected) + print(f'found {len(collected_set)} many channels already collected out of {len(channels)}') + + # remove channels already read + channels = channels - collected_set + channels = list(channels) + print(f'continuing with {len(channels)} many channels, now shuffling...') + + # shuffle channels (again to be safe) + random.shuffle(channels) + + print(f'starting {num_workers} workers now, press ctrl-c to stop') + try: + await launch_workers( + channels, extractors, num_workers = num_workers, block_size = block_size + ) + except (KeyboardInterrupt, Exception): + print(traceback.format_exc()) + + print('number of requests made:', request_count) + + # clean up + completion_file.flush() + completion_file.close() + for extractor in extractors: + extractor.file.flush() + extractor.convert_to_parquet_if_large(force = True) + extractor.file.close() + + + +##################### TOP LEVEL CODE ########################## +# windows specific fix +if os.name == 'nt': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +if __name__ == '__main__': + asyncio.run(main(num_workers = 30, block_size = 500)) diff --git a/code/data/crawl.ipynb b/code/data/crawl.ipynb new file mode 100644 index 0000000..d7d8911 --- /dev/null +++ b/code/data/crawl.ipynb @@ -0,0 +1,254 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from youtubesearchpython import Video\n", + "from copy import deepcopy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# read vid_ids.txt lines to get intial set of urls\n", + "with open('vid_ids.txt', 'r') as f:\n", + " lines = f.readlines()\n", + "unexplored_videos = ['https://www.youtube.com/watch?v=' + line[:-1] for line in lines]\n", + "all_videos = set(deepcopy(unexplored_videos))\n", + "channels = set()\n", + "\n", + "video_file = open('videos.jsonl', 'w')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Pilot study, DFS through youtube recommendations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "link = unexplored_videos.pop()\n", + "new_channels_explored = []\n", + "total_videos_found = len(all_videos)\n", + "playlists = []\n", + "\n", + "while len(unexplored_videos) > 0:\n", + " # use a try block to catch request or parsing errors and skip to next video\n", + " try:\n", + " next_info = Video.getNextInfo(link)\n", + " recommendations = next_info['recommendations']\n", + " assert len(recommendations) > 0, f'no recommendations for {link}'\n", + "\n", + " new_channels = 0\n", + " for recommendation in recommendations:\n", + " # add to unexplored_videos if not already in channels (to stop repeat videos)\n", + " if recommendation['channel']['link'] not in channels:\n", + " channels.add(recommendation['channel']['link'])\n", + " unexplored_videos.append(recommendation['link'])\n", + " new_channels += 1\n", + "\n", + " if recommendation['isPlaylist'] is True:\n", + " playlists.append(recommendation)\n", + " continue\n", + "\n", + " total_videos_found += 1\n", + " all_videos.add(recommendation['link'])\n", + " video_file.write(str(recommendation) + '\\n')\n", + " except Exception as e:\n", + " print(e, link)\n", + " link = unexplored_videos.pop()\n", + " continue\n", + "\n", + " new_channels_explored.append(new_channels)\n", + " link = unexplored_videos.pop()\n", + " print(\n", + " \"channels found:\", len(channels),\n", + " \"unexplored videos left:\", len(unexplored_videos),\n", + " \"total unique videos found:\", len(all_videos),\n", + " \"total videos found:\", total_videos_found,\n", + " end = '\\t\\t\\t\\r'\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### see how number of new channels found changes over time" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "def moving_average(a, n=3):\n", + " ret = np.cumsum(a, dtype=float)\n", + " ret[n:] = ret[n:] - ret[:-n]\n", + " return ret[n - 1:] / n\n", + "\n", + "plt.plot(moving_average(new_channels_explored, 500))\n", + "plt.xlabel('Iterations')\n", + "plt.ylabel('# New Channels Found Per Recommendation (smoothed by 500)')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# close videos.jsonl\n", + "video_file.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import socket\n", + "import copy\n", + "import json\n", + "import os\n", + "\n", + "from aiohttp import ClientSession, TCPConnector, DummyCookieJar\n", + "\n", + "from youtube_helpers import BASE, ENDPOINT, PAYLOAD, USER_AGENT, parse_response\n", + "\n", + "processing_lock = asyncio.Lock()\n", + "\n", + "video_file = open('_v.txt', 'a')\n", + "channel_file = open('_c.txt', 'a')\n", + "playlist_file = open('_p.jsonl', 'a')\n", + "channels = set()\n", + "\n", + "async def get_recommendations(\n", + " video_id, session, unexplored_videos,\n", + " channel_set = channels, lock = processing_lock,\n", + " video_file = video_file, channel_file = channel_file,\n", + " playlist_file = playlist_file\n", + " ):\n", + " data = copy.deepcopy(PAYLOAD)\n", + " data['videoId'] = video_id\n", + " async with session.post(ENDPOINT, headers = {'User-Agent': USER_AGENT},\n", + " json = data, timeout = 5) as response:\n", + " if response.ok is False:\n", + " return response.ok\n", + " \n", + " recommendations = parse_response(await response.json())\n", + " print(recommendations)\n", + " for recommendation in recommendations:\n", + " async with lock:\n", + " video_file.write(recommendation['id'] + '\\n')\n", + "\n", + " if recommendation['isPlaylist']:\n", + " playlist_file.write(json.dumps(recommendation) + '\\n')\n", + " \n", + " if (recommendation['channel']['link'] is not None and\n", + " recommendation['channel']['link'] not in channel_set):\n", + "\n", + " channel_set.add(recommendation['channel']['link'])\n", + " channel_file.write(recommendation['channel']['link'] + '\\n')\n", + "\n", + " if ('shorts' not in recommendation['link'] and\n", + " recommendation['isPlaylist'] is not True):\n", + " unexplored_videos.append(recommendation['id'])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'isPlaylist': False, 'id': 'fFLQc1ueRkQ', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/fFLQc1ueRkQ/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVShBMA8=&rs=AOn4CLD57vNSjbAMU6gTJ1WJNS4gOsIN3A', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/fFLQc1ueRkQ/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVShBMA8=&rs=AOn4CLCb9XSJLJ3o4CixKQFi4gBkJ0JQ7A', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/fFLQc1ueRkQ/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIFUoQTAP&rs=AOn4CLAw-lsoY0eCC938iNgq_Y1_LE9p4w', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/fFLQc1ueRkQ/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIFUoQTAP&rs=AOn4CLAdAOx8MirUlren3AbuYlFiiwNr1A', 'width': 336, 'height': 188}], 'title': 'Murat Aslan | Giresun Karşılaması | Canlı Performans', 'channel': {'name': 'Murat Aslan', 'id': 'UC7ykFyQAvSy-VIbcBM2FfUw', 'link': '/@Murat_Aslan'}, 'duration': '9:25', 'accessibility': {'title': 'Murat Aslan | Giresun Karşılaması | Canlı Performans by Murat Aslan 7 months ago 9 minutes, 25 seconds 16,337 views', 'duration': '9 minutes, 25 seconds'}, 'link': 'https://www.youtube.com/watch?v=fFLQc1ueRkQ', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': True, 'id': 'RDnWe3tW-XtaY', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/nWe3tW-XtaY/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB_gmAAtAFigIMCAAQARhyIFooQDAP&rs=AOn4CLCafKi7fTgtJuZi1Y_rAkBtlNEEsA', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/nWe3tW-XtaY/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB_gmAAtAFigIMCAAQARhyIFooQDAP&rs=AOn4CLCcKajbE8nsVVwmOuBt5y0Uw5VNjw', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/nWe3tW-XtaY/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYciBaKEAwDw==&rs=AOn4CLAWZd2sIIA-Phs9iJCSI7O8dsHZRg', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/nWe3tW-XtaY/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYciBaKEAwDw==&rs=AOn4CLBWbFbQNPnaJ1rZ-oqBgptAvHxsrA', 'width': 336, 'height': 188}], 'title': 'Mix - Murat Aslan - Doğancan Yıldırım | Giresun Karşılaması | Canlı Performans', 'channel': {'name': None, 'id': None, 'link': None}, 'duration': None, 'accessibility': {'title': None, 'duration': None}, 'link': 'https://www.youtube.com/watch?v=KPCBVw8zAQk&list=RDnWe3tW-XtaY&start_radio=1&rv=nWe3tW-XtaY', 'isPlayable': None, 'videoCount': None}, {'isPlaylist': False, 'id': 'uZAM69S93Q0', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/uZAM69S93Q0/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHWBIAC4AOKAgwIABABGHIgRCgxMA8=&rs=AOn4CLBHOsqCEw1q77MXXwJu5nlSvxlEmA', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/uZAM69S93Q0/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHWBIAC4AOKAgwIABABGHIgRCgxMA8=&rs=AOn4CLB7fS9hrspMcZ9t3MAEwEXFYlMR5g', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/uZAM69S93Q0/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB1gSAAuADigIMCAAQARhyIEQoMTAP&rs=AOn4CLAY8GDa-r5cdE1S3FB8ISAXZQ5_2Q', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/uZAM69S93Q0/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB1gSAAuADigIMCAAQARhyIEQoMTAP&rs=AOn4CLDj_jiT8R2oN-D0EdyEFyD2syQjkA', 'width': 336, 'height': 188}], 'title': 'ilker kabayel - Hiçbir Şeyde Gözüm Yok (1991) Berlin Türk Sanat Müziği Okulu', 'channel': {'name': 'ilker kabayel', 'id': 'UCnaTEGQp4Dw2bo7DPc57d8Q', 'link': '/channel/UCnaTEGQp4Dw2bo7DPc57d8Q'}, 'duration': '6:44', 'accessibility': {'title': 'ilker kabayel - Hiçbir Şeyde Gözüm Yok (1991) Berlin Türk Sanat Müziği Okulu by ilker kabayel 7 hours ago 6 minutes, 44 seconds 140 views', 'duration': '6 minutes, 44 seconds'}, 'link': 'https://www.youtube.com/watch?v=uZAM69S93Q0', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'KPCBVw8zAQk', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/KPCBVw8zAQk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAqgVag9f94Kmui9DD2Tr9VPYb-IA', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/KPCBVw8zAQk/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDl1MBXgiHkoGtjWl76PCA0AaE9NQ', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/KPCBVw8zAQk/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBsO0K9Ijd1mxDALFmhfux6gj9fhg', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/KPCBVw8zAQk/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCPF_F6KYwaGXiYS0adpcaKw3dwEg', 'width': 336, 'height': 188}], 'title': 'Görelenin İçinde', 'channel': {'name': 'Hüseyin Bıçak - Topic', 'id': 'UCwsHT-s_re1UUMjtPbJiYwg', 'link': '/channel/UCwsHT-s_re1UUMjtPbJiYwg'}, 'duration': '5:25', 'accessibility': {'title': 'Görelenin İçinde by Hüseyin Bıçak - Topic 7 years ago 5 minutes, 25 seconds 115,950 views', 'duration': '5 minutes, 25 seconds'}, 'link': 'https://www.youtube.com/watch?v=KPCBVw8zAQk', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'fLYw94CTL6M', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/fLYw94CTL6M/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGEYgZShgMA8=&rs=AOn4CLD_LD2_rWWQH8x-b--XScgRUGHElg', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/fLYw94CTL6M/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGEYgZShgMA8=&rs=AOn4CLCPRLbj4uETzmogbgiHqUV8D6J4dQ', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/fLYw94CTL6M/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhGIGUoYDAP&rs=AOn4CLC0uuuOnb6KWQw7jNDXJNQ5cSPi0w', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/fLYw94CTL6M/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhGIGUoYDAP&rs=AOn4CLBOuG34n1olGep3ZCnWOgQT-BPyNg', 'width': 336, 'height': 188}], 'title': 'Murat Aslan Işıl Işıl Giresun Karşılaması 2023 21 Dakika', 'channel': {'name': 'Hasan Yayında', 'id': 'UCRaLemo6M0haUrwGWTQoM1A', 'link': '/@hasanyaynda2313'}, 'duration': '21:19', 'accessibility': {'title': 'Murat Aslan Işıl Işıl Giresun Karşılaması 2023 21 Dakika by Hasan Yayında 5 months ago 21 minutes 13,189 views', 'duration': '21 minutes, 19 seconds'}, 'link': 'https://www.youtube.com/watch?v=fLYw94CTL6M', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': '_X0eCu5CJLo', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/_X0eCu5CJLo/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgOig0MA8=&rs=AOn4CLB2xjOhKwKD7b_MAJlFZt0iVLZjPA', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/_X0eCu5CJLo/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgOig0MA8=&rs=AOn4CLA8FaET6F-dOaq52OCgSUUSGZc8hg', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/_X0eCu5CJLo/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhyIDooNDAP&rs=AOn4CLAq4aUD73DzzihUVA5Pgc8gwIS-zw', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/_X0eCu5CJLo/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhyIDooNDAP&rs=AOn4CLBVpej5O5QACChyVSIUFrB4ddIgOg', 'width': 336, 'height': 188}], 'title': 'Selin • Ferhat | düğün | giresun karşılaması | piraziz / giresun |', 'channel': {'name': 'kahramanarslanfotoğraf', 'id': 'UC4eWX3iW-GKLjpov880H0Fg', 'link': '/@kahramanarslanfotograf'}, 'duration': '4:19', 'accessibility': {'title': 'Selin • Ferhat | düğün | giresun karşılaması | piraziz / giresun | by kahramanarslanfotoğraf 1 year ago 4 minutes, 19 seconds 7,650 views', 'duration': '4 minutes, 19 seconds'}, 'link': 'https://www.youtube.com/watch?v=_X0eCu5CJLo', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'fq5J5ZtctU4', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/fq5J5ZtctU4/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGD0gRihyMA8=&rs=AOn4CLDSpMoJQqL6tmEE_Ku_A1TDX8aYNQ', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/fq5J5ZtctU4/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGD0gRihyMA8=&rs=AOn4CLDhKd674m8uzgnhiSqacXQHKPAaHg', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/fq5J5ZtctU4/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARg9IEYocjAP&rs=AOn4CLAnGrmGEsWKDc4uaFPYxQvNEzqL7g', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/fq5J5ZtctU4/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARg9IEYocjAP&rs=AOn4CLCXj_I66Zd3Vi-97GTRQM-XWKTgOw', 'width': 336, 'height': 188}], 'title': 'Murat Aslan | Giresun Karşılaması | Canlı Performans', 'channel': {'name': 'Murat Aslan', 'id': 'UC7ykFyQAvSy-VIbcBM2FfUw', 'link': '/@Murat_Aslan'}, 'duration': '14:20', 'accessibility': {'title': 'Murat Aslan | Giresun Karşılaması | Canlı Performans by Murat Aslan 7 months ago 14 minutes, 20 seconds 15,489 views', 'duration': '14 minutes, 20 seconds'}, 'link': 'https://www.youtube.com/watch?v=fq5J5ZtctU4', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'abt_77U7MfY', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/abt_77U7MfY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCC2-J2fkDa7OhtCS4vZ0g4SgWrJQ', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/abt_77U7MfY/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAA7jy42mL3yCHgPG85KTNlYZnRQQ', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/abt_77U7MfY/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAo942ghoiC_K4lClmWODdVRfTaGg', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/abt_77U7MfY/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCtN-m3yt6z6iXsn1F-912RPdJhBg', 'width': 336, 'height': 188}], 'title': 'ELİFÇE+++++++++++HORON POTPORİ', 'channel': {'name': 'YAŞA GİRESUN YAŞA 28', 'id': 'UCxrelJgFSEoiyilRhzPXs7A', 'link': '/@yasagiresunyasa2872'}, 'duration': '15:07', 'accessibility': {'title': 'ELİFÇE+++++++++++HORON POTPORİ by YAŞA GİRESUN YAŞA 28 7 years ago 15 minutes 32,055 views', 'duration': '15 minutes, 7 seconds'}, 'link': 'https://www.youtube.com/watch?v=abt_77U7MfY', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'qwGHcZDA2G4', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/qwGHcZDA2G4/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgYyhPMA8=&rs=AOn4CLClsIB5M5dv0PuJ74qr-6vYE4grPg', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/qwGHcZDA2G4/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgYyhPMA8=&rs=AOn4CLB2wT5MuB7HvexO_0Gqa2rocEmqrg', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/qwGHcZDA2G4/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIGMoTzAP&rs=AOn4CLA1aBmjZTDxfQMOROBI2lDmDMdjlA', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/qwGHcZDA2G4/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIGMoTzAP&rs=AOn4CLCGug29Occ1dy456PrdH7ataolsBw', 'width': 336, 'height': 188}], 'title': 'Murat Aslan | Efe Karagüzel | Atma Türküler - Uzun Hava', 'channel': {'name': 'Murat Aslan', 'id': 'UC7ykFyQAvSy-VIbcBM2FfUw', 'link': '/@Murat_Aslan'}, 'duration': '8:42', 'accessibility': {'title': 'Murat Aslan | Efe Karagüzel | Atma Türküler - Uzun Hava by Murat Aslan 9 months ago 8 minutes, 42 seconds 24,971 views', 'duration': '8 minutes, 42 seconds'}, 'link': 'https://www.youtube.com/watch?v=qwGHcZDA2G4', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': 'va029SIrqHc', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/va029SIrqHc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAGia5JnoCEsZ5pXoI7JPDByXA_NA', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/va029SIrqHc/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBiZVcVRXf3Itw90hsU3zU9nTlaYg', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/va029SIrqHc/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBLaQ2QUIZ_G_Cs3v4Pf_FZHkduDw', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/va029SIrqHc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCV3rJKuKBP0zElaOytLmPuBdJuhg', 'width': 336, 'height': 188}], 'title': 'ŞABAN YAĞMUR IŞIL IŞIL EMRAH KUMAŞ GİRESUN KARŞILAMA POTPORİ', 'channel': {'name': 'Hasan Yayında', 'id': 'UCRaLemo6M0haUrwGWTQoM1A', 'link': '/@hasanyaynda2313'}, 'duration': '13:37', 'accessibility': {'title': 'ŞABAN YAĞMUR IŞIL IŞIL EMRAH KUMAŞ GİRESUN KARŞILAMA POTPORİ by Hasan Yayında 3 years ago 13 minutes, 37 seconds 91,612 views', 'duration': '13 minutes, 37 seconds'}, 'link': 'https://www.youtube.com/watch?v=va029SIrqHc', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': '44MrAteeZs8', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/44MrAteeZs8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCl3lRSF5St_0Wz1TMKSeS6GjTfJw', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/44MrAteeZs8/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDYt9gHZelsmBzLX1OlIwcJHUhv3A', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/44MrAteeZs8/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD5jdBm4l9pjuLPZ_2Rl_HtbW3_Cw', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/44MrAteeZs8/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAMfsbWPBiGuYkMZyJExWQgBXV8Kw', 'width': 336, 'height': 188}], 'title': 'GİRESUN KARŞILAMASI HİCABİ ŞENLİKOĞLU 2023 POTPORİ', 'channel': {'name': 'Hasan Yayında', 'id': 'UCRaLemo6M0haUrwGWTQoM1A', 'link': '/@hasanyaynda2313'}, 'duration': '7:57', 'accessibility': {'title': 'GİRESUN KARŞILAMASI HİCABİ ŞENLİKOĞLU 2023 POTPORİ by Hasan Yayında 4 months ago 7 minutes, 57 seconds 22,560 views', 'duration': '7 minutes, 57 seconds'}, 'link': 'https://www.youtube.com/watch?v=44MrAteeZs8', 'isPlayable': None, 'videoCount': 1}, {'isPlaylist': False, 'id': '9d591mqcP1E', 'thumbnails': [{'url': 'https://i.ytimg.com/vi/9d591mqcP1E/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVihQMA8=&rs=AOn4CLCq0KC7XBP4UqnLjChwb9_qVhX0jw', 'width': 168, 'height': 94}, {'url': 'https://i.ytimg.com/vi/9d591mqcP1E/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVihQMA8=&rs=AOn4CLCKjocL3dEv0bl8TuBSbiSdrdGxug', 'width': 196, 'height': 110}, {'url': 'https://i.ytimg.com/vi/9d591mqcP1E/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIFYoUDAP&rs=AOn4CLCesMWk_GERwqRu96hCz43_1aLrnw', 'width': 246, 'height': 138}, {'url': 'https://i.ytimg.com/vi/9d591mqcP1E/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIFYoUDAP&rs=AOn4CLAd63GhL0vKWmAnogf6xoJoZmsMAQ', 'width': 336, 'height': 188}], 'title': 'dereli uşakları iş başında #vol5', 'channel': {'name': 'Dereli uşak', 'id': 'UCdYhTmshsMxRo2rre18dQ8g', 'link': '/@dereliusak'}, 'duration': '11:13', 'accessibility': {'title': 'dereli uşakları iş başında #vol5 by Dereli uşak 11 months ago 11 minutes, 13 seconds 10,720 views', 'duration': '11 minutes, 13 seconds'}, 'link': 'https://www.youtube.com/watch?v=9d591mqcP1E', 'isPlayable': None, 'videoCount': 1}]\n" + ] + } + ], + "source": [ + "async with ClientSession(base_url = BASE) as session:\n", + " video_id = \"nWe3tW-XtaY\"\n", + " await get_recommendations(video_id, session, [])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/code/data/youtube_helpers.py b/code/data/youtube_helpers.py new file mode 100644 index 0000000..56a5633 --- /dev/null +++ b/code/data/youtube_helpers.py @@ -0,0 +1,424 @@ +import json +import re +from datetime import datetime + +from dateutil.relativedelta import relativedelta +from bs4 import BeautifulSoup as bs4 + + + +# constants used for requests +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0" +BASE = 'https://www.youtube.com' +ENDPOINT = '/youtubei/v1/next?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false' +BROWSE_ENDPOINT = '/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false' +HEADER = { + 'User-Agent': USER_AGENT, + 'keep_alive': 'True', +} +PAYLOAD = { + "context": { + "client": { + "clientName": "WEB", + "clientVersion": "2.20230921.01.00", + "newVisitorCookie": True, + }, + "user": { + "lockedSafetyMode": False, + } + } +} + + + +def getValue(source, path): + value = source + for key in path: + if type(key) is str: + if key in value.keys(): + value = value[key] + else: + value = None + break + elif type(key) is int: + if len(value) != 0: + value = value[key] + else: + value = None + break + return value + + +def parse_response(response): + videorenderers = getValue(response, ["playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results"]) + videos = [] + + if videorenderers is None: + return videos + + for video in videorenderers: + if "endScreenVideoRenderer" in video.keys(): + video = video["endScreenVideoRenderer"] + j = { + "isPlaylist" : False, + "id": getValue(video, ["videoId"]), + "thumbnails": getValue(video, ["thumbnail", "thumbnails"]), + "title": getValue(video, ["title", "simpleText"]), + "channel": { + "name": getValue(video, ["shortBylineText", "runs", 0, "text"]), + "id": getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId"]), + "link": getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "canonicalBaseUrl"]), + }, + "duration": getValue(video, ["lengthText", "simpleText"]), + "accessibility": { + "title": getValue(video, ["title", "accessibility", "accessibilityData", "label"]), + "duration": getValue(video, ["lengthText", "accessibility", "accessibilityData", "label"]), + }, + "link": "https://www.youtube.com" + getValue(video, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), + "isPlayable": getValue(video, ["isPlayable"]), + "videoCount": 1, + } + videos.append(j) + + if "endScreenPlaylistRenderer" in video.keys(): + video = video["endScreenPlaylistRenderer"] + j = { + "isPlaylist" : True, + "id": getValue(video, ["playlistId"]), + "thumbnails": getValue(video, ["thumbnail", "thumbnails"]), + "title": getValue(video, ["title", "simpleText"]), + "channel": { + "name": getValue(video, ["shortBylineText", "runs", 0, "text"]), + "id": getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId"]), + "link": getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "canonicalBaseUrl"]), + }, + "duration": getValue(video, ["lengthText", "simpleText"]), + "accessibility": { + "title": getValue(video, ["title", "accessibility", "accessibilityData", "label"]), + "duration": getValue(video, ["lengthText", "accessibility", "accessibilityData", "label"]), + }, + "link": "https://www.youtube.com" + getValue(video, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), + "isPlayable": getValue(video, ["isPlayable"]), + "videoCount": getValue(video, ["videoCount"]), + } + videos.append(j) + return videos + + +denominations = { + 'K': 1000, + 'M': 1000000, + 'B': 1000000000 + } +def text_to_num(text, demoninations = denominations): + if text[-1].upper() in denominations: + # separate out the K, M, or B + num, magnitude = text[:-1], text[-1] + return int(float(num) * denominations[magnitude]) + else: + return int(text) + + +def parse_date(date): + val, unit = date.split()[:2] + if not unit.endswith('s'): + unit = unit + 's' + past_time = datetime.now() - relativedelta(**{unit:int(val)}) + return past_time.strftime("%Y-%m-%d") + + +def get_channel_id(html): + soup = bs4(html, 'html.parser') + data_str = 'var ytInitialData = ' + channel_data = json.loads( + soup(text = re.compile(data_str))[0].strip(data_str).strip(';') + ) + services = channel_data['responseContext']['serviceTrackingParams'] + for services in services: + for param in services['params']: + if param['key'] == 'browse_id': + return param['value'] + assert False, "Unable to find channel id" + + +def get_tab(response, name): + tabs = getValue(response, ['contents', 'twoColumnBrowseResultsRenderer', 'tabs']) + # if tabs is None: + # print(response) + for tab in tabs: + if 'tabRenderer' in tab and getValue(tab, ['tabRenderer', 'title']) == name: + return getValue(tab, ['tabRenderer', 'content']) + + +def get_continuation(response): + continuation = getValue( + response, ['onResponseReceivedActions', 0, 'appendContinuationItemsAction', 'continuationItems'] + ) + + # continuation returned empty (i.e. no more videos to collect) + if 'responseContext' in response and continuation is None: + return None + + return continuation + + +################################### TAB PARSERS #################################### +def parse_about(channel_link, channel_id = None, response = None, tab = None, **kwargs): + title = getValue(response, ["metadata", "channelMetadataRenderer", "title"]) + + subscribers = getValue( + response, + ["header", "c4TabbedHeaderRenderer", "subscriberCountText", "simpleText"] + ) + if subscribers is not None: + subscribers = text_to_num(subscribers.split(' ')[0]) + + num_vids_shorts = getValue(response, ["header", "c4TabbedHeaderRenderer", "videosCountText", "runs", 0, "text"]) + if num_vids_shorts is not None: + num_vids_shorts = text_to_num(num_vids_shorts.split(' ')[0].replace(',', '').replace('No', '0')) + + description = getValue(response, ["metadata", "channelMetadataRenderer", "description"]) + isFamilySafe = getValue(response, ["metadata", "channelMetadataRenderer", "isFamilySafe"]) + + monetization = None + for param in getValue(response, ['responseContext', 'serviceTrackingParams']): + if param.get('service') == 'GFEEDBACK': + for feedback in param['params']: + key = feedback.get('key') + if key == 'is_monetization_enabled': + monetization = (feedback.get('value') == 'true') + + verified = False + badges = getValue(response, ['header', 'c4TabbedHeaderRenderer', 'badges']) + if badges is not None: + for badge in badges: + if getValue(badge, ['metadataBadgeRenderer', 'tooltip']) == 'Verified': + verified = True + + full_meta = getValue(tab, ['sectionListRenderer', 'contents', 0, 'itemSectionRenderer', 'contents', 0, 'channelAboutFullMetadataRenderer']) + view_count = getValue(full_meta, ['viewCountText', 'simpleText']) + if view_count is not None: + view_count = text_to_num(view_count.split(' ')[0].replace(',', '')) + + join_date = getValue(full_meta, ['joinedDateText', 'runs', 1, 'text']) + if join_date is not None: + join_date = datetime.strptime(join_date, '%b %d, %Y').strftime("%Y-%m-%d") + + country = getValue(full_meta, ['country', 'simpleText']) + + tags = getValue(response, ["microformat", "microformatDataRenderer", "tags"]) + + fullsize_avatar = getValue(full_meta, ['avatar', 'thumbnails', 0, 'url']) + if fullsize_avatar is not None: + fullsize_avatar = fullsize_avatar.split('=')[0] + + fullsize_banner = getValue(response, ["header", "c4TabbedHeaderRenderer", "banner", 'thumbnails', 0, 'url']) + if fullsize_banner is not None: + fullsize_banner = fullsize_banner.split('=')[0] + + links = full_meta.get('links') + parsed_links = None + if links is not None: + parsed_links = [] + for link in links: + link_title = getValue(link, ['channelExternalLinkViewModel', 'title', 'content']) + link_url = getValue(link, ['channelExternalLinkViewModel', 'link', 'content']) + parsed_links.append({'title': link_title, 'link': link_url}) + parsed_links + + # use 2d array to match the format of other parsers) + return [[channel_link, channel_id, title, subscribers, view_count, num_vids_shorts, join_date, country, monetization, verified, + isFamilySafe, description, tags, fullsize_avatar, fullsize_banner, parsed_links]], None + + + +def parse_videos(channel_link, tab = None, continuation = None, **kwargs): + video_infos = None + if continuation is None: + # initial get request, ensure there are videos to begin with + video_infos = getValue( + tab, ['richGridRenderer', 'contents'] + ) + + # video tab doesn't contain videos + if video_infos is None: + return None, None + else: + video_infos = continuation + + assert video_infos is not None, "Unable to find videos in response" + + video_rows = [] + token = None + for info in video_infos: + if "richItemRenderer" in info: + video_data = getValue(info, ['richItemRenderer', 'content', 'videoRenderer']) + + if 'publishedTimeText' not in video_data: + # unpublished video for the future + continue + + id = video_data["videoId"] + title = getValue(video_data, ['title', 'runs', 0, 'text']) + publish = parse_date(video_data['publishedTimeText']['simpleText'].replace('Streamed ', '')) + length = getValue(video_data, ['lengthText', 'simpleText']) + views = getValue(video_data, ['viewCountText', 'simpleText']) + if views is not None: + views = int(views.split(' ')[0].replace(',', '').replace('No', '0')) + description_snippet = getValue(video_data, ['descriptionSnippet', 'runs', 0, 'text']) + + thumbnails = getValue(video_data, ['thumbnail', 'thumbnails']) + moving_thumbnails = getValue(video_data, ['richThumbnail', 'movingThumbnailRenderer', 'movingThumbnailDetails', 'thumbnails']) + + + video_rows.append([channel_link, id, title, publish, length, views, description_snippet, moving_thumbnails, thumbnails]) + if 'continuationItemRenderer' in info: + token = getValue(info, ['continuationItemRenderer', 'continuationEndpoint', 'continuationCommand', 'token']) + return video_rows, token + + +def parse_shorts(channel_link, tab = None, continuation = None, **kwargs): + if continuation is None: + # initial request, ensure there are shorts to begin with + shorts_info = getValue( + tab, ['richGridRenderer', 'contents'] + ) + + # shorts tab empty + if shorts_info is None: + return None, None + else: + shorts_info = continuation + + assert shorts_info is not None, "Unable to find shorts in response" + + shorts = [] + token = None + for renderer in shorts_info: + if "richItemRenderer" in renderer: + short_content = getValue(renderer, ["richItemRenderer", "content", "reelItemRenderer"]) + id = getValue(short_content, ["videoId"]) + headline = getValue(short_content, ["headline", "simpleText"]) + + thumbnail = getValue(short_content, ["thumbnail", "thumbnails", 0]) + width = None + height = None + if thumbnail is not None and 'url' in thumbnail: + width = getValue(thumbnail, ["width"]) + height = getValue(thumbnail, ["height"]) + thumbnail = getValue(thumbnail, ["url"]) + + viewCountText = getValue(short_content, ["viewCountText", "simpleText"]) + if viewCountText is not None: + viewCountText = text_to_num(viewCountText.split(' ')[0].replace(',', '').replace('No', '0')) + + length = getValue(short_content, ["accessibility", "accessibilityData", "label"]) + if length is not None: + length = length.split(' - ')[1] + + shorts.append([channel_link, id, headline, viewCountText, length, thumbnail, width, height]) + if 'continuationItemRenderer' in renderer: + token = getValue(renderer, ['continuationItemRenderer', 'continuationEndpoint', 'continuationCommand', 'token']) + return shorts, token + + +def parse_live(channel_link, tab = None, continuation = None, **kwargs): + return parse_videos(channel_link, tab = tab, continuation = continuation) + + +def parse_playlists(channel_link, tab = None, continuation = None, **kwargs): + if continuation is None: + # initial request, ensure there are playlists to begin with + playlist_infos = getValue(tab, + ['sectionListRenderer', 'contents', 0, 'itemSectionRenderer', 'contents', 0, 'gridRenderer', 'items'] + ) + + # playlist tab empty + if playlist_infos is None: + return None, None + else: + playlist_infos = continuation + + assert playlist_infos is not None, "Unable to find playlists in response" + + playlists = [] + token = None + for info in playlist_infos: + if 'gridPlaylistRenderer' in info: + info = info['gridPlaylistRenderer'] + id = getValue(info, ['playlistId']) + title = getValue(info, ['title', 'runs', 0, 'text']) + num_videos = getValue(info, ['videoCountShortText', 'simpleText']) + + playlists.append([channel_link, id, title, num_videos]) + if 'continuationItemRenderer' in info: + token = getValue(info, ['continuationItemRenderer', 'continuationEndpoint', 'continuationCommand', 'token']) + + return playlists, token + + +def parse_featured_channels(channel_link, tab = None, continuation = None, **kwargs): + if continuation is None: + # initial request, ensure there are featured channels to begin with + channel_infos = getValue(tab, + ['sectionListRenderer', 'contents', 0, 'itemSectionRenderer', 'contents', 0, 'gridRenderer', 'items'] + ) + + # featured channels tab empty + if channel_infos is None: + return None, None + else: + channel_infos = continuation + + assert channel_infos is not None, "Unable to find featured channels in response" + + channels = [] + token = None + for info in channel_infos: + if 'gridChannelRenderer' in info: + info = info['gridChannelRenderer'] + id = getValue(info, ['channelId']) + url = getValue(info, ['navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url']) + name = getValue(info, ['title', 'simpleText']) + subscribers = getValue(info, ['subscriberCountText', 'simpleText']) + if subscribers is not None: + subscribers = text_to_num(subscribers.split(' ')[0].replace(',', '').replace('No', '0')) + num_shorts_vids = getValue(info, ['videoCountText', 'runs', 0, 'text']) + + channels.append([channel_link, id, url, name, subscribers, num_shorts_vids]) + if 'continuationItemRenderer' in info: + token = getValue(info, ['continuationItemRenderer', 'continuationEndpoint', 'continuationCommand', 'token']) + return channels, token + + + +# organize the parsers +tab_helpers = [ + { + 'name': 'About', 'filename': 'channels', 'param': 'EgVhYm91dPIGBAoCEgA%3D', 'parse_func': parse_about, + 'features': [ + 'link', 'id', 'title', 'subscribers', 'view_count', 'num_vids_shorts', 'join_date', 'country', 'monetization', 'verified', + 'isFamilySafe', 'description', 'tags', 'fullsize_avatar', 'fullsize_banner', 'parsed_links' + ] + }, + { + 'name': 'Videos', 'filename': 'videos', 'param': 'EgZ2aWRlb3PyBgQKAjoA', 'parse_func': parse_videos, + 'features': ['link', 'id', 'title', 'approx_date', 'length', 'views', 'description_snippet', 'moving_thumbnails', 'thumbnails'] + }, + { + 'name': 'Shorts', 'filename': 'shorts', 'param': 'EgZzaG9ydHPyBgUKA5oBAA%3D%3D', 'parse_func': parse_shorts, + 'features': ['link', 'id', 'headline', 'viewCountText', 'length', 'thumbnail', 'width', 'height'] + }, + { + 'name': 'Live', 'filename': 'livestreams', 'param': 'EgdzdHJlYW1z8gYECgJ6AA%3D%3D', 'parse_func': parse_live, + 'features': ['link', 'id', 'title', 'approx_date', 'length', 'views', 'description_snippet', 'moving_thumbnails', 'thumbnails'] + }, + { + 'name': 'Playlists', 'filename': 'playlists', 'param': 'EglwbGF5bGlzdHPyBgQKAkIA', 'parse_func': parse_playlists, + 'features': ['link', 'id', 'title', 'num_videos'] + }, + { + 'name': 'Channels', 'filename': 'featured_channels', 'param': 'EghjaGFubmVsc_IGBAoCUgA%3D', 'parse_func': parse_featured_channels, + 'features': ['link', 'id', 'url', 'name', 'subscribers', 'num_shorts_vids'] + }, +] diff --git a/code/filtering/cache/sample.csv b/code/filtering/cache/sample.csv new file mode 100644 index 0000000..51b6a32 --- /dev/null +++ b/code/filtering/cache/sample.csv @@ -0,0 +1,154 @@ +channel_link,id,title,date,length,views,description_snippet + +/@JournalistAshrafAliMaitlo,opCOKEK3kr8,"The shopkeeper of Ahmedpur was robbed from the hands of Vyaji, due to coercion, he had taken 3 lakh",2023-09-13,1:26,0, + +/@JournalistAshrafAliMaitlo,Le95Ybz_JIU,Radio FM92,2023-08-31,2:07,2, + +/@JournalistAshrafAliMaitlo,k6LQTDDbIVA,NADRA has taken a big step for the convenience of citizens,2023-08-31,1:44,6, + +/@JournalistAshrafAliMaitlo,e5zJMVZOfik,May the Lord do justice to the holy girl,2023-08-31,2:23,1, + +/@JournalistAshrafAliMaitlo,lHARgEHc9QA,FM92,2023-08-24,2:08,1, + +/@JournalistAshrafAliMaitlo,fBvwvBgWMso,"Ahmedpur: The pain of the thieves could not be reduced, the thief stole the motorcycle parked",2023-08-24,0:52,1, + +/@JournalistAshrafAliMaitlo,rhfIdPLghBI,Babar Ka Azab,2023-08-24,1:37,2, + +/@JournalistAshrafAliMaitlo,VpwD-YaU3Yw,"Allah in the day, Allah in the night",2023-08-24,6:48,4, + +/@JournalistAshrafAliMaitlo,3iHJU-6YPLA,14 august,2023-08-14,0:40,74, + +/@JournalistAshrafAliMaitlo,oWd1uj4KQa0,"The house of justice for the poor, poor, helpless woman",2023-08-14,4:17,9, + +/@JournalistAshrafAliMaitlo,RPfEE30320I,"Ahmedpur, many villages and crops are under water due to continuous flow of Indus river",2023-08-14,2:33,8, + +/@JournalistAshrafAliMaitlo,XJQGusjBlho,"June 29, 2023",2023-07-14,0:52,3, + +/@JournalistAshrafAliMaitlo,Xso1NDn52tc,"June 29, 2023",2023-07-14,0:43,0, + +/@JournalistAshrafAliMaitlo,Cts67NlniIo,مبارڪون مبارڪون مبارڪون عيد جون لک لک مبارڪون,2023-07-14,0:54,0, + +/@JournalistAshrafAliMaitlo,E-6Kx4C7rzk,Poet Faqir Ali Khan Mitlo will not forget to listen to the glory of the dowry Benazir Income Sapot.,2023-07-14,2:27,0, + +/@JournalistAshrafAliMaitlo,pOtmqAwnJtQ,10 people were injured in a fight between two factions of the Sahita community Sahita near Ahmedpur.,2023-07-14,1:46,0, + +/@nadeshdatta,WXS9mj8i3Z4,shiva ravana stotram,2022-09-14,4:08,1009, + +/@nadeshdatta,MMzfQVRfb68,శ్రీ దుర్గ మాత,2022-09-14,0:41,353, + +/@nadeshdatta,Qh9Wxo5bmPo,"శ్రీ రామ ,కృష్ణ మంత్రం",2022-09-14,1:10,862, + +/@nadeshdatta,IyrhCdEQn_0,దశావతారాలలో ఒక్కొక్క అవతార నామాన్ని స్మరిస్తే కలిగే ఫలితాలు.. వరాహ పురాణం నుండీ..,2022-09-14,4:27,1312, + +/@nadeshdatta,2BrGtYMrY6Q,Jaya Jayahe Mahishasura Mardini Ramya Kapardini Shaila Sute…….,2022-09-14,17:02,1990,"Jaya Jayahe Mahishasura Mardini Ramya Kapardini Shaila Sute……. + +It was really an enlightening and highly informative discourse with a scintillating and spirit raising bhajan session this..." + +/@nadeshdatta,I4pXwbhF_9g,వరాహ పురాణంలో శివుడు చేసిన అమ్మ వారి ప్రార్థన,2022-09-14,5:18,510, + +/@nadeshdatta,AMrvAu84ln4,పితృదేవతలకు శ్రద్ధ సమయంలో ..,2022-09-14,5:05,1416, + +/@nadeshdatta,rc7NpwfFWMg,ప్రయాణిస్తున్నప్పుడు పఠించవలసిన మంత్రం,2022-09-14,0:37,569, + +/@nadeshdatta,RpE1sNDAigQ,చిన్న పిల్లల దోషాలు పోవడానికి ?,2022-09-14,0:34,194, + +/@nadeshdatta,IOrU6dzHdV8,ద్వాదశ అదిత్యుల నామాలు,2022-09-14,0:36,127, + +/@nadeshdatta,469UErdZ_TE,దుర్గ మంత్రం,2022-09-14,0:41,176, + +/@nadeshdatta,rq3zphI-FhY,( వారాహి మాత),2022-09-14,0:34,160, + +/@nadeshdatta,walFDWF46g8,Guru Purnima • KSHT_ Frisco_ TX • 13 Jul 2022,2022-09-14,58:51,2131, + +/@nadeshdatta,psKQo7Rd92I,కార్య సిద్ది హనుమాన్ మంత్రం,2022-09-14,2:23,1203, + +/@nadeshdatta,gw5xBFOaOVo,అంజనమ్మ తపసు చేసి. Divya Nama Sankeertana • SGS,2022-09-14,5:59,2730,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022" + +/@nadeshdatta,bP1woRFAPBg,గోవింద నామము మధురం మధురం. Divya Nama Sankeertana • SGS.,2022-09-14,6:57,5971,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 +గోవింద నామము మధురం మధురం భవ జలధి తారకం శుభ దాయ..." + +/@nadeshdatta,63Xrarpb2Lc,దిశి దిశి సం వససి విభో Divya Nama Sankeertana • SGS.,2022-09-14,6:39,5289,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 +దిశి దిశి సం వససి విభో వస వస మే హృది శివ భో గిరి..." + +/@nadeshdatta,gyp7GvxWPxQ,దత్త దిగంబర జయ జయ దత్తా. Divya Nama Sankeertana,2022-09-14,11:05,6833,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 + +దత్త దిగంబర జయ జయ దత్తా దత్త నిరంజన జయ జయ దత్త..." + +/@nadeshdatta,0KuLUQJYsUE,ఓం నమస్తే గణపతేయై. Divya Nama Sankeertana.,2022-09-14,4:08,1415,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022" + +/@nadeshdatta,6lnxsytFZ20,గాయేఁ హంసబ్ మిల్కే తేరి. DivyaNama Sankeertana • SGS,2022-09-14,10:37,1557,"గాయేఁ హంసబ్ మిల్కే తేరి మహిమాకి గాథా + +పల్లవి: గాయేఁ హంసబ్ మిల్కే తేరి మహిమ..." + +/@nadeshdatta,uBL_RHttp64,హరి హరి హరి హరి హరి యనరా Divya Nama Sankeertana • SGS,2022-09-14,6:55,4313,"హరిహరి హరిహరి హరి యనరా హరి + +పల్లవి: హరిహరి హరిహరి హరి యనరా హరి పాదాలే శరణమ్..." + +/@nadeshdatta,tmPqoVwIKR0,దత్తా దత్తా అనుకొందాం Divya NamaSankeertana -SGS,2022-09-14,11:25,4376,"దత్తా దత్తా అనుకొందాం ఆనందంతో + +పల్లవి : దత్తా దత్తా అనుకొందాం ఆనందంతో యెగ..." + +/@nadeshdatta,iVAaK2Icpww,రావోయి రావోయి రావోయి ఉగాది,2022-09-14,7:45,773,"రావోయి రావోయి రావోయి ఉగాది + +పల్లవి: రావోయి రావోయి రావోయి ఉగాది శుభకృతికీ..." + +/@nadeshdatta,pBw5y1nOHVc,మాలా కమండలు ధరః కర పద్మ యుగ్మేుగళే డమరు త్రిశూలేయసస్ఊర్వయోశుభ శంఖ చక్రేవందేం అతివరద భుజషట్కయుక్తం,2022-09-14,2:54,853,"మాలా కమండలు ధరః కర పద్మ యుగ్మే + +మధ్యస్థపాణి యుగళే డమరు త్రిశూలే + +యస్యస్త..." + +/@nadeshdatta,uJRB0eeMlpk,"Shubhakrut Yugadi ,Happy Tune ,SGS Ashram, Hyderabad",2022-09-14,16:38,662, + +/@nadeshdatta,X0AigqR0KRw,మహా భారతంలో శివ ప్రార్థన,2022-09-14,5:17,616,"మహా భారతంలో శివ ప్రార్థన +గానం :పరమ పూజ్య శ్రీ శ్రీ శ్రీ గణపతి సచ్చిదానంద..." + +/@nadeshdatta,SpbP9e5_IHQ,శ్రీ దుర్గా ద్వాత్రింశన్నామ మాలా *_Sri Durga Dwatrimsha Namamala Stotram_,2022-09-14,7:21,1616,"*శ్రీ దుర్గా ద్వాత్రింశన్నామ మాలా* + *_Sri Durga Dwatrimsha Namamala Stotram_* +_దుర్గా దుర్గార్తి..." + +/@nadeshdatta,Y5RZDsOk5O4,కృష్ణ లీల కీర్తనమ్. Divya Nama Sankeertana - SGS (6 మే 2021,2022-09-14,7:12,660, + +/@nadeshdatta,c_iqFurP8dY,కృష్ణా హరి మాధవ Divya Nama Sankeertana • SGS.,2022-09-14,11:47,449, + +/@nadeshdatta,s4S5Sm-ABQk,శివ పద భజన. Divya Nama Sankeertana • SGS,2022-09-14,2:46,342, + +/@davizinematheuzin11,626UKXY6kFc,jogando stumble guys pela primeira vez nesse canal e eu consegui a coroa do stumble guys,2023-09-11,5:03,4,"joguei stumble guys pela primeira nesse canal é eu consegui a coroa do stumble guys🍷🗿🍷🗿é joguei bem demaise 👇🏼👇🏼👇🏼 + + + + + + + + +desça mais👇🏼👇🏼👇🏼..." + +/@davizinematheuzin11,TPGcZ6qQU2w,como fazer rabiola de pipa,2023-09-08,1:52,4,"aprenda a fazer rabiola de pipa (Pode ser de arraia ,caixão morcego e entre outros)de forma fácil com apenas sacola ,linha de pipa e a ,sua pipa 🪁🪁🪁🪁🗿🍷 + + + + + + + + + + + + +deixa..." + +/@davizinematheuzin11,dOWKfW-dNUs,Sonic Dash,2023-09-03,2:54,13,perdi contra o boas + +/@davizinematheuzin11,nHclxz4ScI8,jogando Shark evolution (de novo) e comi todo mundo,2023-08-31,5:02,43,"jogando Shark evolution (de novo) e comi todo mundo(lá ele) jogando com o tubarão martelo 🐳🐋🐬🐟🐠🐡🦈🦈🦈🐙🐚 + + + + + + + + + +deixa o like aíííí please" diff --git a/code/filtering/cache/test.csv b/code/filtering/cache/test.csv new file mode 100644 index 0000000..f1bd9d4 --- /dev/null +++ b/code/filtering/cache/test.csv @@ -0,0 +1,16 @@ +channel_link,id,title,date,length,views,description_snippet +/@JournalistAshrafAliMaitlo,Cts67NlniIo,مبارڪون مبارڪون مبارڪون عيد جون لک لک مبارڪون,2023-07-14,0:54,0, +/@nadeshdatta,pBw5y1nOHVc,మాలా కమండలు ధరః కర పద్మ యుగ్మేుగళే డమరు త్రిశూలేయసస్ఊర్వయోశుభ శంఖ చక్రేవందేం అతివరద భుజషట్కయుక్తం,2022-09-14,2:54,853,"మాలా కమండలు ధరః కర పద్మ యుగ్మే + +మధ్యస్థపాణి యుగళే డమరు త్రిశూలే + +యస్యస్త..." +/@nadeshdatta,gw5xBFOaOVo,అంజనమ్మ తపసు చేసి. Divya Nama Sankeertana • SGS,2022-09-14,5:59,2730,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022" +/@nadeshdatta,s4S5Sm-ABQk,శివ పద భజన. Divya Nama Sankeertana • SGS,2022-09-14,2:46,342, +/@nadeshdatta,MMzfQVRfb68,శ్రీ దుర్గ మాత,2022-09-14,0:41,353, +/@davizinematheuzin11,dOWKfW-dNUs,Sonic Dash,2023-09-03,2:54,13,perdi contra o boas +/@nadeshdatta,469UErdZ_TE,దుర్గ మంత్రం,2022-09-14,0:41,176, +/@nadeshdatta,IOrU6dzHdV8,ద్వాదశ అదిత్యుల నామాలు,2022-09-14,0:36,127, +/@nadeshdatta,63Xrarpb2Lc,దిశి దిశి సం వససి విభో Divya Nama Sankeertana • SGS.,2022-09-14,6:39,5289,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 +దిశి దిశి సం వససి విభో వస వస మే హృది శివ భో గిరి..." +/@nadeshdatta,IyrhCdEQn_0,దశావతారాలలో ఒక్కొక్క అవతార నామాన్ని స్మరిస్తే కలిగే ఫలితాలు.. వరాహ పురాణం నుండీ..,2022-09-14,4:27,1312, diff --git a/code/filtering/cache/train.csv b/code/filtering/cache/train.csv new file mode 100644 index 0000000..08821e1 --- /dev/null +++ b/code/filtering/cache/train.csv @@ -0,0 +1,89 @@ +channel_link,id,title,date,length,views,description_snippet +/@JournalistAshrafAliMaitlo,Xso1NDn52tc,"June 29, 2023",2023-07-14,0:43,0, +/@JournalistAshrafAliMaitlo,lHARgEHc9QA,FM92,2023-08-24,2:08,1, +/@nadeshdatta,tmPqoVwIKR0,దత్తా దత్తా అనుకొందాం Divya NamaSankeertana -SGS,2022-09-14,11:25,4376,"దత్తా దత్తా అనుకొందాం ఆనందంతో + +పల్లవి : దత్తా దత్తా అనుకొందాం ఆనందంతో యెగ..." +/@JournalistAshrafAliMaitlo,3iHJU-6YPLA,14 august,2023-08-14,0:40,74, +/@JournalistAshrafAliMaitlo,e5zJMVZOfik,May the Lord do justice to the holy girl,2023-08-31,2:23,1, +/@JournalistAshrafAliMaitlo,rhfIdPLghBI,Babar Ka Azab,2023-08-24,1:37,2, +/@nadeshdatta,X0AigqR0KRw,మహా భారతంలో శివ ప్రార్థన,2022-09-14,5:17,616,"మహా భారతంలో శివ ప్రార్థన +గానం :పరమ పూజ్య శ్రీ శ్రీ శ్రీ గణపతి సచ్చిదానంద..." +/@davizinematheuzin11,626UKXY6kFc,jogando stumble guys pela primeira vez nesse canal e eu consegui a coroa do stumble guys,2023-09-11,5:03,4,"joguei stumble guys pela primeira nesse canal é eu consegui a coroa do stumble guys🍷🗿🍷🗿é joguei bem demaise 👇🏼👇🏼👇🏼 + + + + + + + + +desça mais👇🏼👇🏼👇🏼..." +/@davizinematheuzin11,TPGcZ6qQU2w,como fazer rabiola de pipa,2023-09-08,1:52,4,"aprenda a fazer rabiola de pipa (Pode ser de arraia ,caixão morcego e entre outros)de forma fácil com apenas sacola ,linha de pipa e a ,sua pipa 🪁🪁🪁🪁🗿🍷 + + + + + + + + + + + + +deixa..." +/@JournalistAshrafAliMaitlo,pOtmqAwnJtQ,10 people were injured in a fight between two factions of the Sahita community Sahita near Ahmedpur.,2023-07-14,1:46,0, +/@JournalistAshrafAliMaitlo,oWd1uj4KQa0,"The house of justice for the poor, poor, helpless woman",2023-08-14,4:17,9, +/@nadeshdatta,WXS9mj8i3Z4,shiva ravana stotram,2022-09-14,4:08,1009, +/@nadeshdatta,RpE1sNDAigQ,చిన్న పిల్లల దోషాలు పోవడానికి ?,2022-09-14,0:34,194, +/@nadeshdatta,0KuLUQJYsUE,ఓం నమస్తే గణపతేయై. Divya Nama Sankeertana.,2022-09-14,4:08,1415,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022" +/@nadeshdatta,bP1woRFAPBg,గోవింద నామము మధురం మధురం. Divya Nama Sankeertana • SGS.,2022-09-14,6:57,5971,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 +గోవింద నామము మధురం మధురం భవ జలధి తారకం శుభ దాయ..." +/@JournalistAshrafAliMaitlo,opCOKEK3kr8,"The shopkeeper of Ahmedpur was robbed from the hands of Vyaji, due to coercion, he had taken 3 lakh",2023-09-13,1:26,0, +/@nadeshdatta,c_iqFurP8dY,కృష్ణా హరి మాధవ Divya Nama Sankeertana • SGS.,2022-09-14,11:47,449, +/@nadeshdatta,rq3zphI-FhY,( వారాహి మాత),2022-09-14,0:34,160, +/@nadeshdatta,gyp7GvxWPxQ,దత్త దిగంబర జయ జయ దత్తా. Divya Nama Sankeertana,2022-09-14,11:05,6833,"Divya Nama Sankeertana • SGS Ashrama, BHEL, Hyderabad • 14 April 2022 + +దత్త దిగంబర జయ జయ దత్తా దత్త నిరంజన జయ జయ దత్త..." +/@JournalistAshrafAliMaitlo,fBvwvBgWMso,"Ahmedpur: The pain of the thieves could not be reduced, the thief stole the motorcycle parked",2023-08-24,0:52,1, +/@nadeshdatta,psKQo7Rd92I,కార్య సిద్ది హనుమాన్ మంత్రం,2022-09-14,2:23,1203, +/@JournalistAshrafAliMaitlo,XJQGusjBlho,"June 29, 2023",2023-07-14,0:52,3, +/@nadeshdatta,uBL_RHttp64,హరి హరి హరి హరి హరి యనరా Divya Nama Sankeertana • SGS,2022-09-14,6:55,4313,"హరిహరి హరిహరి హరి యనరా హరి + +పల్లవి: హరిహరి హరిహరి హరి యనరా హరి పాదాలే శరణమ్..." +/@JournalistAshrafAliMaitlo,Le95Ybz_JIU,Radio FM92,2023-08-31,2:07,2, +/@nadeshdatta,I4pXwbhF_9g,వరాహ పురాణంలో శివుడు చేసిన అమ్మ వారి ప్రార్థన,2022-09-14,5:18,510, +/@JournalistAshrafAliMaitlo,k6LQTDDbIVA,NADRA has taken a big step for the convenience of citizens,2023-08-31,1:44,6, +/@nadeshdatta,Y5RZDsOk5O4,కృష్ణ లీల కీర్తనమ్. Divya Nama Sankeertana - SGS (6 మే 2021,2022-09-14,7:12,660, +/@nadeshdatta,6lnxsytFZ20,గాయేఁ హంసబ్ మిల్కే తేరి. DivyaNama Sankeertana • SGS,2022-09-14,10:37,1557,"గాయేఁ హంసబ్ మిల్కే తేరి మహిమాకి గాథా + +పల్లవి: గాయేఁ హంసబ్ మిల్కే తేరి మహిమ..." +/@nadeshdatta,rc7NpwfFWMg,ప్రయాణిస్తున్నప్పుడు పఠించవలసిన మంత్రం,2022-09-14,0:37,569, +/@nadeshdatta,uJRB0eeMlpk,"Shubhakrut Yugadi ,Happy Tune ,SGS Ashram, Hyderabad",2022-09-14,16:38,662, +/@JournalistAshrafAliMaitlo,RPfEE30320I,"Ahmedpur, many villages and crops are under water due to continuous flow of Indus river",2023-08-14,2:33,8, +/@nadeshdatta,AMrvAu84ln4,పితృదేవతలకు శ్రద్ధ సమయంలో ..,2022-09-14,5:05,1416, +/@nadeshdatta,Qh9Wxo5bmPo,"శ్రీ రామ ,కృష్ణ మంత్రం",2022-09-14,1:10,862, +/@davizinematheuzin11,nHclxz4ScI8,jogando Shark evolution (de novo) e comi todo mundo,2023-08-31,5:02,43,"jogando Shark evolution (de novo) e comi todo mundo(lá ele) jogando com o tubarão martelo 🐳🐋🐬🐟🐠🐡🦈🦈🦈🐙🐚 + + + + + + + + + +deixa o like aíííí please" +/@nadeshdatta,2BrGtYMrY6Q,Jaya Jayahe Mahishasura Mardini Ramya Kapardini Shaila Sute…….,2022-09-14,17:02,1990,"Jaya Jayahe Mahishasura Mardini Ramya Kapardini Shaila Sute……. + +It was really an enlightening and highly informative discourse with a scintillating and spirit raising bhajan session this..." +/@JournalistAshrafAliMaitlo,VpwD-YaU3Yw,"Allah in the day, Allah in the night",2023-08-24,6:48,4, +/@nadeshdatta,SpbP9e5_IHQ,శ్రీ దుర్గా ద్వాత్రింశన్నామ మాలా *_Sri Durga Dwatrimsha Namamala Stotram_,2022-09-14,7:21,1616,"*శ్రీ దుర్గా ద్వాత్రింశన్నామ మాలా* + *_Sri Durga Dwatrimsha Namamala Stotram_* +_దుర్గా దుర్గార్తి..." +/@JournalistAshrafAliMaitlo,E-6Kx4C7rzk,Poet Faqir Ali Khan Mitlo will not forget to listen to the glory of the dowry Benazir Income Sapot.,2023-07-14,2:27,0, +/@nadeshdatta,walFDWF46g8,Guru Purnima • KSHT_ Frisco_ TX • 13 Jul 2022,2022-09-14,58:51,2131, +/@nadeshdatta,iVAaK2Icpww,రావోయి రావోయి రావోయి ఉగాది,2022-09-14,7:45,773,"రావోయి రావోయి రావోయి ఉగాది + +పల్లవి: రావోయి రావోయి రావోయి ఉగాది శుభకృతికీ..." diff --git a/code/filtering/config.yaml b/code/filtering/config.yaml new file mode 100644 index 0000000..85c7be8 --- /dev/null +++ b/code/filtering/config.yaml @@ -0,0 +1,13 @@ +# Hyperparameters for Quality Score +w1: 0.6 +w2: 0.4 + +# Choice of metric for video movement (can be SSIM or MSE) +metric: SSIM + +# video2dataset configuration +url_col: url +caption_col: caption + +# Data Split +test_size: 0.2 diff --git a/code/filtering/extract.py b/code/filtering/extract.py new file mode 100644 index 0000000..cc985b8 --- /dev/null +++ b/code/filtering/extract.py @@ -0,0 +1,74 @@ +""" +Generate a sample CSV file of size r from the original CSV file. +Generate train and test CSV files from the original CSV file. +""" +import csv +import os +import argparse +from sklearn.model_selection import train_test_split + +def extract_first_r_entries(input_csv, output_csv, r): + """ + Extracts the first r entries from the input CSV and writes them to the output CSV. + + Args: + - input_csv (str): Path to the input CSV file. + - output_csv (str): Path to the output CSV file. + - r (int): Number of entries to extract. + """ + with open(input_csv, mode='r') as infile, open(output_csv, mode='w', newline='') as outfile: + reader = csv.reader(infile) + writer = csv.writer(outfile) + + # Write header + writer.writerow(next(reader)) + + # Write first r entries + for _ in range(r): + try: + writer.writerow(next(reader)) + except StopIteration: + break + +def split_data(input_csv_path, train_csv_path, test_csv_path, test_size=0.2): + """ + Split the provided data into train and test datasets. + """ + with open(input_csv_path, mode='r') as infile: + reader = list(csv.DictReader(infile)) + train_data, test_data = train_test_split(reader, test_size=test_size, random_state=42) + + with open(train_csv_path, mode='w', newline='') as trainfile: + writer = csv.DictWriter(trainfile, fieldnames=reader[0].keys()) + writer.writeheader() + for row in train_data: + writer.writerow(row) + + with open(test_csv_path, mode='w', newline='') as testfile: + writer = csv.DictWriter(testfile, fieldnames=reader[0].keys()) + writer.writeheader() + for row in test_data: + writer.writerow(row) + + + +if __name__ == "__main__": + r = 100 + + input_csv="sample/videos.csv" # original file + output_csv = "cache/sample.csv" + + if os.path.exists(output_csv): + print(f"File {output_csv} already exists. Skipping.") + else: + extract_first_r_entries(input_csv, output_csv, r) + + + import argparse + parser = argparse.ArgumentParser(description='Compute quality scores for videos.') + parser.add_argument('input_csv', help='Path to the input CSV file containing video data.') + parser.add_argument('train_csv', help='Path to the train CSV file.') + parser.add_argument('test_csv', help='Path to the test CSV file.') + args = parser.parse_args() + + split_data(args.input_csv, args.train_csv, args.test_csv) \ No newline at end of file diff --git a/code/filtering/linear.py b/code/filtering/linear.py new file mode 100644 index 0000000..b3fd518 --- /dev/null +++ b/code/filtering/linear.py @@ -0,0 +1,102 @@ +import wandb +import csv +import yaml +import numpy as np +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_squared_error +from sklearn.model_selection import train_test_split +from utils import compute_quality_score +import joblib + +# Initialize wandb +wandb.init(project="video_quality_prediction") + +def load_data(input_csv_path, config): + """ + Load data from the provided CSV file and compute quality scores if needed. + + Args: + - input_csv_path (str): Path to the input CSV file containing video data. + + Returns: + - X (np.array): Features for each video. + - y (np.array): Quality scores for each video. + """ + data = [] + with open(input_csv_path, mode='r') as infile: + reader = csv.DictReader(infile) + data = [row for row in reader] + + # Check if "quality_score" column exists + if "quality_score" not in data[0]: + # Compute quality scores and add them to the data + for row in data: + video_id = row["id"] + quality_score = compute_quality_score(video_id, config) # This function is from utils.py + row["quality_score"] = quality_score + + # Write the updated data back to the CSV file + with open(input_csv_path, mode='w', newline='') as outfile: + writer = csv.DictWriter(outfile, fieldnames=data[0].keys()) + writer.writeheader() + for row in data: + writer.writerow(row) + + # Extract features and quality scores for training + features = [] + quality_scores = [] + for row in data: + video_id = row["id"] + quality_score = float(row["quality_score"]) + feature_vector = [ + float(row["length"].split(":")[0]), # Taking video length in minutes as a feature + ] + features.append(feature_vector) + quality_scores.append(quality_score) + + return np.array(features), np.array(quality_scores) + + +def train_model(input_csv_path, model_path, config_path): + """ + Train a Linear Regression model on the provided data. + + Args: + - input_csv_path (str): Path to the input CSV file containing video data. + - model_path (str): Path to save the trained model. + - config_path (str): Path to the config.yaml file. + """ + + with open(config_path, 'r') as ymlfile: + config = yaml.load(ymlfile, Loader=yaml.FullLoader) + + X, y = load_data(input_csv_path, config) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=config["split_test_size"], random_state=42) + + model = LinearRegression() + model.fit(X_train, y_train) + + # Predict on test data + y_pred = model.predict(X_test) + + # Compute MSE + mse_val = mean_squared_error(y_test, y_pred) + + # Log metrics to wandb + wandb.log({"MSE": mse_val}) + + # Save the model locally + joblib.dump(model, model_path) + + return model + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description='Train a Linear Regression model for video quality prediction.') + parser.add_argument('--input_csv', help='Path to the input CSV file containing video data.') + parser.add_argument('--model_path', help='Path to save the trained model.') + parser.add_argument('--config', help='Path to the config.yaml file.') + args = parser.parse_args() + + train_model(args.input_csv, args.model_path, args.config) diff --git a/code/filtering/main.ipynb b/code/filtering/main.ipynb new file mode 100644 index 0000000..edd5a60 --- /dev/null +++ b/code/filtering/main.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# split into train and test\n", + "!python3 utils.py --input_csv data/sample.csv --train_csv data/train.csv --test_csv data/test.csv\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"/Users/tomohirosawada/Library/Mobile Documents/com~apple~CloudDocs/From_Desktop/desktop_buffer/videorl/code/filtering/models/linear.py\", line 5, in \n", + " from sklearn.linear_model import LinearRegression\n", + "ModuleNotFoundError: No module named 'sklearn'\n" + ] + } + ], + "source": [ + "# train model\n", + "!python3 models/linear.py data/train.csv cache config.yaml\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "duckai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/code/filtering/todo.md b/code/filtering/todo.md new file mode 100644 index 0000000..25d189c --- /dev/null +++ b/code/filtering/todo.md @@ -0,0 +1,11 @@ + +- Extract train/test file in order to train model to predict video quality model + - Currently only have baseline linear regression model (See chatgpt for other candidates ) +- After getting model that qualitiatvley behaves well, write script for getting descriptive stats of the video + + +START HERE: + +- utils.py is throwing an error when I try to run it on Google colab. Speicifically mock_utils is not defined. + - Understand what is wrong with it +- diff --git a/code/filtering/utils.py b/code/filtering/utils.py new file mode 100644 index 0000000..f5a7ed0 --- /dev/null +++ b/code/filtering/utils.py @@ -0,0 +1,95 @@ +import imageio +import requests +from io import BytesIO + +import csv +import yaml +import os +import json + +from skimage.transform import resize +from skimage.metrics import structural_similarity as ssim +from skimage.metrics import mean_squared_error as mse + +from video2dataset import video2dataset + +def compute_similarity(img_url1, img_url2, metric): + """ + Compute the similarity between two images based on the specified metric. + """ + # Download the images from the URLs + img1 = imageio.imread(BytesIO(requests.get(img_url1).content)) + img2 = imageio.imread(BytesIO(requests.get(img_url2).content)) + + # Resize the images to the smallest shape between the two + target_shape = tuple(min(s1, s2) for s1, s2 in zip(img1.shape, img2.shape)) + img1 = resize(img1, target_shape) + img2 = resize(img2, target_shape) + + if metric == "SSIM": + return ssim(img1, img2, multichannel=True) + elif metric == "MSE": + return mse(img1, img2) + else: + raise ValueError(f"Unsupported metric: {metric}") + +def compute_quality_score(video_id, config): + """ + Compute the quality score for a given video ID based on the provided configuration. + """ + base_url = "https://img.youtube.com/vi/" + # URLs for the four generated images from YouTube + img_urls = [f"{base_url}{video_id}/{i}.jpg" for i in range(4)] + + # Compute pairwise differences + diffs = [] + for i in range(3): + for j in range(i+1, 4): + diffs.append(compute_similarity(img_urls[i], img_urls[j], config["metric"])) + + movement_score = sum(diffs) / len(diffs) + + # For now, using a placeholder for resolution. In a real-world scenario, the resolution would be fetched. + resolution = 1080 + + # Compute quality score + quality_score = config["w1"] * movement_score + config["w2"] * resolution + + return quality_score + + +def main(input_csv_path, output_csv_path, config_path): + """ + Main function to compute quality scores for all videos and save to the output CSV. + """ + with open(config_path, 'r') as ymlfile: + config = yaml.load(ymlfile, Loader=yaml.FullLoader) + + # Use video2dataset to download videos and save them to a designated folder + video2dataset(url_list=input_csv_path, + output_folder="dataset", + url_col=config["url_col"], + caption_col=config["caption_col"]) + + with open(input_csv_path, mode='r') as infile, open(output_csv_path, mode='w', newline='') as outfile: + reader = csv.DictReader(infile) + fieldnames = reader.fieldnames + ["quality_score"] + writer = csv.DictWriter(outfile, fieldnames=fieldnames) + writer.writeheader() + + for row in reader: + video_id = row["id"] + + # Assuming video files are saved with format {video_id}.mp4 and metadata in {video_id}.json + video_file_path = os.path.join("dataset", f"{video_id}.mp4") + metadata_file_path = os.path.join("dataset", f"{video_id}.json") + + with open(metadata_file_path, 'r') as json_file: + metadata = json.load(json_file) + + # Use metadata if necessary for further processing + + quality_score = compute_quality_score(video_file_path, config) + row["quality_score"] = quality_score + writer.writerow(row) +