Compare commits
103
Commits
0.0.1
..
3ca58b2466
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca58b2466 | ||
|
|
20025e02b4 | ||
|
|
e493eaeb49 | ||
|
|
e1aff6f469 | ||
|
|
ff5542d8b0 | ||
|
|
65bc8853e6 | ||
|
|
d3c87d3b6f | ||
|
|
58f25a114e | ||
|
|
3bba97e90c | ||
|
|
9d433ba2f3 | ||
|
|
572f9e121f | ||
|
|
02c3d74eb6 | ||
|
|
95e96d08e1 | ||
|
|
df3af2780b | ||
|
|
68cd27c263 | ||
|
|
f44ee4b3c1 | ||
|
|
7626c7bad1 | ||
|
|
76e41f2f7b | ||
|
|
8d4f31d106 | ||
|
|
c0db4eb437 | ||
|
|
e23a0fc4c7 | ||
|
|
e5dd476490 | ||
|
|
fd75d27b99 | ||
|
|
cb7b68342a | ||
|
|
e166fdb4e5 | ||
|
|
8d34cf33ae | ||
|
|
09f7675bf7 | ||
|
|
4449d488c9 | ||
|
|
34c0d03587 | ||
|
|
8d20327b67 | ||
|
|
09c2a8eafe | ||
|
|
9cd7c57e2f | ||
|
|
94ad823e3b | ||
|
|
454503271e | ||
|
|
54fe40e588 | ||
|
|
8cf5698051 | ||
|
|
04c59eb025 | ||
|
|
9b792015fa | ||
|
|
4508b12b6c | ||
|
|
ea604cce40 | ||
|
|
01aac3f66e | ||
|
|
d0dee043c6 | ||
|
|
d5c55e756a | ||
|
|
50306aff5d | ||
|
|
4fa433495b | ||
|
|
a0ba474fcc | ||
|
|
b4cb072770 | ||
|
|
fda8fc728e | ||
|
|
82e597f205 | ||
|
|
03c2566a20 | ||
|
|
a116b29df9 | ||
|
|
1fe92ec2d0 | ||
|
|
411a3db465 | ||
|
|
08b465753d | ||
|
|
45c5d8a940 | ||
|
|
668aa608ed | ||
|
|
c746d43ddf | ||
|
|
05cd243abd | ||
|
|
9d40fef429 | ||
|
|
d34fade0cd | ||
|
|
68b1f9a77f | ||
|
|
c17a33d181 | ||
|
|
59dc5676cc | ||
|
|
548a797ee8 | ||
|
|
46ef8146f8 | ||
|
|
4133faad41 | ||
|
|
fced9e0a3a | ||
|
|
f114aac0cf | ||
|
|
7456685f2b | ||
|
|
270c48da02 | ||
|
|
f6c11b54f3 | ||
|
|
27cd5c3a64 | ||
|
|
eb805f5ced | ||
|
|
d6f6b26361 | ||
|
|
b3779fe49a | ||
|
|
3ae0f4e924 | ||
|
|
3758405f52 | ||
|
|
71e4bd95a9 | ||
|
|
34bbc216f6 | ||
|
|
baec8fcb6c | ||
|
|
773f86c551 | ||
|
|
f436007836 | ||
|
|
dfbf38b071 | ||
|
|
292dd8919e | ||
|
|
dab4c8ff69 | ||
|
|
9d5c9fde2a | ||
|
|
f201ae7a91 | ||
|
|
4cae7e09d1 | ||
|
|
e4b4ce21a2 | ||
|
|
a2990c7699 | ||
|
|
923c14a77b | ||
|
|
73a9dea023 | ||
|
|
938ae4b0fb | ||
|
|
c50a82df1b | ||
|
|
36f1fb9e83 | ||
|
|
934954092a | ||
|
|
eaae954e1b | ||
|
|
2ffc1d0e5d | ||
|
|
53acda6abe | ||
|
|
d776c09fec | ||
|
|
daf14d4b5b | ||
|
|
3aa9947f8e | ||
|
|
c828b118d3 |
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
|
A video introducing this project is available [here](https://crawler.yt.lemnoslife.com/presentation).
|
||||||
|
|
||||||
# The algorithm:
|
# The algorithm:
|
||||||
|
|
||||||
To retrieve the most YouTube video ids in order to retrieve the most video captions, we need to retrieve the most YouTube channels.
|
To retrieve the most YouTube video ids in order to retrieve the most video captions, we need to retrieve the most YouTube channels.
|
||||||
@@ -9,6 +11,24 @@ A ready to be used by the end-user website instance of this project is hosted at
|
|||||||
|
|
||||||
See more details on [the Wiki](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/wiki).
|
See more details on [the Wiki](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/wiki).
|
||||||
|
|
||||||
|
# The project structure:
|
||||||
|
|
||||||
|
- `main.cpp` contains the C++ multi-threaded algorithm proceeding to the YouTube channels discovery. It is notably made of the following functions:
|
||||||
|
- `main` which takes into account the command line arguments, load variables from files (`channels.txt`, `keys.txt`, `channels/` content) and start the threads as executing `treatChannels` function
|
||||||
|
- `treatChannels` gets a YouTube channel to treat, treat it in `treatChannelOrVideo` function and compress the retrieved data
|
||||||
|
- `treatChannelOrVideo` which provided a YouTube channel id or a video id, treats this resource. In both cases it treats comments left on this resource. In the case of a channel it also treats its `CHANNELS`, `COMMUNITY`, `PLAYLISTS` and `LIVE` tabs and downloads the captions of the channel videos.
|
||||||
|
- `markChannelAsRequiringTreatmentIfNeeded` which provided a YouTube channel id marks it as requiring treatment if it wasn't already treated
|
||||||
|
- `execute` which provided an `yt-dlp` command executes it in a shell
|
||||||
|
- `getJson` which provided an API request returns a JSON structure with its result. In the case that the API requested is YouTube Data API v3 and a set of keys is provided (see below `keys.txt`), it rotates the keys as required
|
||||||
|
- `channels.txt` contains a starting set of channels which contains mostly the 100 most subscribed French channels
|
||||||
|
- `keys.txt` contains a set of YouTube Data API v3 keys (not provided) to have the ability to request this API (see an alternative to filling it in the section below with `--no-keys` command line argument)
|
||||||
|
- `scripts/` contains Python scripts to:
|
||||||
|
- generate the `channels.txt` as described above (`retrieveTop100SubscribersFrance.py`)
|
||||||
|
- remove channels being treated before a restart of the algorithm as described in [the `main` function documentation](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/src/commit/8dd89e6e881da0a905b6fa4b23775c4344dd0d9d/main.cpp#L126-L128) (`removeChannelsBeingTreated.py`)
|
||||||
|
- `website/` is a PHP website using WebSocket to allow the end-user to proceed to requests on the retrieved dataset. When fetching the website, the end-user receives the interpreted `index.php` which upon making a request interacts with `websocket.php` which in the back-end dispatches the requests from various end-users to `search.py` (which treats the actual end-user request on the compressed dataset) by using `users/` to make the inter-process communication.
|
||||||
|
|
||||||
|
Note that this project heavily relies on [YouTube operational API](https://github.com/Benjamin-Loison/YouTube-operational-API) [which was modified for this project](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/wiki/YouTube-operational-API-commits).
|
||||||
|
|
||||||
# Running the YouTube graph discovery algorithm:
|
# Running the YouTube graph discovery algorithm:
|
||||||
|
|
||||||
Because of [the current compression mechanism](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/issues/30), Linux is the only known OS able to run this algorithm.
|
Because of [the current compression mechanism](https://gitea.lemnoslife.com/Benjamin_Loison/YouTube_captions_search_engine/issues/30), Linux is the only known OS able to run this algorithm.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
PREFIX = 'Comments per second: '
|
PREFIX = 'Channels per second: '
|
||||||
alreadyTreatedCommentsCount = 0
|
alreadyTreatedCommentsCount = 0
|
||||||
|
|
||||||
with open('nohup.out') as f:
|
with open('nohup.out') as f:
|
||||||
@@ -8,5 +8,7 @@ with open('nohup.out') as f:
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
if PREFIX in line:
|
if PREFIX in line:
|
||||||
alreadyTreatedCommentsCount += int(line.split(PREFIX)[-1])
|
alreadyTreatedCommentsCount += int(line.split(PREFIX)[-1])
|
||||||
|
#if 'UCsT0YIqwnpJCM-mx7-gSA4Q' in line:
|
||||||
|
# break
|
||||||
|
|
||||||
print(alreadyTreatedCommentsCount)
|
print(alreadyTreatedCommentsCount)
|
||||||
+8
-4
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# This algorithm should also take in account other features that we use to retrieve channels.
|
||||||
|
|
||||||
import os, requests, json, time, datetime
|
import os, requests, json, time, datetime
|
||||||
|
|
||||||
path = 'channels/'
|
path = 'channels/'
|
||||||
@@ -12,11 +14,13 @@ def getTimestampFromDateString(dateString):
|
|||||||
for channelId in list(os.walk('.'))[1]:
|
for channelId in list(os.walk('.'))[1]:
|
||||||
channelId = channelId[2:]
|
channelId = channelId[2:]
|
||||||
#print(channelId)
|
#print(channelId)
|
||||||
numberOfRequests = len(list(os.walk(channelId))[0][2])
|
numberOfRequests = len(list(os.walk(f'{channelId}/requests'))[0][2]) - 1
|
||||||
# Assume that the folder isn't empty (may not be the case, but it is most of the time).
|
# Assume that the folder isn't empty (may not be the case, but it is most of the time).
|
||||||
with open(f'{channelId}/{str(numberOfRequests - 1)}.json') as f:
|
filePath = f'{channelId}/requests/{str(numberOfRequests - 1)}.json'
|
||||||
content = "\n".join(f.read().splitlines()[1:])
|
with open(filePath) as f:
|
||||||
data = json.loads(content)
|
print(filePath)
|
||||||
|
#content = "\n".join(f.read().splitlines()[1:])
|
||||||
|
data = json.load(f)#json.loads(content)
|
||||||
snippet = data['items'][-1]['snippet']
|
snippet = data['items'][-1]['snippet']
|
||||||
if 'topLevelComment' in snippet:
|
if 'topLevelComment' in snippet:
|
||||||
snippet = snippet['topLevelComment']['snippet']
|
snippet = snippet['topLevelComment']['snippet']
|
||||||
File renamed without changes.
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import os, requests, json
|
import os, requests, json
|
||||||
|
|
||||||
channelIds = next(os.walk('channels/'))[1]
|
channelIds = [channelId.replace('.zip', '') for channelId in next(os.walk('channels/'))[2]]
|
||||||
maxResults = 50
|
maxResults = 50
|
||||||
|
|
||||||
channelIdsChunks = [channelIds[i : i + maxResults] for i in range(0, len(channelIds), maxResults)]
|
channelIdsChunks = [channelIds[i : i + maxResults] for i in range(0, len(channelIds), maxResults)]
|
||||||
File renamed without changes.
@@ -1,4 +1,7 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
# We can't proceed automatically by using `requests` Python module because https://socialblade.com/youtube/top/country/fr/mostsubscribed is protected by CloudFlare.
|
# We can't proceed automatically by using `requests` Python module because https://socialblade.com/youtube/top/country/fr/mostsubscribed is protected by CloudFlare.
|
||||||
|
# Note that `undetected-chromedriver` might be a workaround this limitation.
|
||||||
|
|
||||||
with open('mostsubscribed.html') as f:
|
with open('mostsubscribed.html') as f:
|
||||||
lines = f.read().splitlines()
|
lines = f.read().splitlines()
|
||||||
+17
-3
@@ -10,6 +10,11 @@ pathSearchMessageParts = sys.argv[2].split(' ')
|
|||||||
pathSearch = pathSearchMessageParts[1]
|
pathSearch = pathSearchMessageParts[1]
|
||||||
message = ' '.join(pathSearchMessageParts[2:])
|
message = ' '.join(pathSearchMessageParts[2:])
|
||||||
|
|
||||||
|
pathSearchRegex = re.compile(pathSearch)
|
||||||
|
messageRegex = re.compile(message)
|
||||||
|
|
||||||
|
isPathSearchAChannelId = re.match(r'[a-zA-Z0-9-_]{24}', pathSearch)
|
||||||
|
|
||||||
searchOnlyCaptions = pathSearchMessageParts[0] == 'search-only-captions'
|
searchOnlyCaptions = pathSearchMessageParts[0] == 'search-only-captions'
|
||||||
|
|
||||||
clientFilePath = f'users/{clientId}.txt'
|
clientFilePath = f'users/{clientId}.txt'
|
||||||
@@ -33,23 +38,31 @@ def cleanCaption(caption):
|
|||||||
return caption.replace('\n', ' ')
|
return caption.replace('\n', ' ')
|
||||||
|
|
||||||
# As `zipgrep` doesn't support arguments to stop on first match for each file, we proceed manually to keep a good theoretical complexity.
|
# As `zipgrep` doesn't support arguments to stop on first match for each file, we proceed manually to keep a good theoretical complexity.
|
||||||
|
if isPathSearchAChannelId:
|
||||||
|
file = pathSearch + '.zip'
|
||||||
|
if os.path.isfile(path + file):
|
||||||
|
files = [file]
|
||||||
|
else:
|
||||||
|
write(f'progress:0 / 0')
|
||||||
|
else:
|
||||||
files = [file for file in os.listdir(path) if file.endswith('.zip')]
|
files = [file for file in os.listdir(path) if file.endswith('.zip')]
|
||||||
|
|
||||||
for fileIndex, file in enumerate(files):
|
for fileIndex, file in enumerate(files):
|
||||||
write(f'progress:{fileIndex + 1} / {len(files)}')
|
write(f'progress:{fileIndex} / {len(files)}')
|
||||||
zip = zipfile.ZipFile(path + file)
|
zip = zipfile.ZipFile(path + file)
|
||||||
for fileInZip in zip.namelist():
|
for fileInZip in zip.namelist():
|
||||||
endsWithVtt = fileInZip.endswith('.vtt')
|
endsWithVtt = fileInZip.endswith('.vtt')
|
||||||
if searchOnlyCaptions and not endsWithVtt:
|
if searchOnlyCaptions and not endsWithVtt:
|
||||||
continue
|
continue
|
||||||
toWrite = f'{file}/{fileInZip}'
|
toWrite = f'{file}/{fileInZip}'
|
||||||
if not bool(re.search(pathSearch, toWrite)):
|
if not bool(pathSearchRegex.search(toWrite)):
|
||||||
continue
|
continue
|
||||||
with zip.open(fileInZip) as f:
|
with zip.open(fileInZip) as f:
|
||||||
if endsWithVtt:
|
if endsWithVtt:
|
||||||
content = f.read().decode('utf-8')
|
content = f.read().decode('utf-8')
|
||||||
stringIOf = StringIO(content)
|
stringIOf = StringIO(content)
|
||||||
wholeCaption = ' '.join([cleanCaption(caption.text) for caption in webvtt.read_buffer(stringIOf)])
|
wholeCaption = ' '.join([cleanCaption(caption.text) for caption in webvtt.read_buffer(stringIOf)])
|
||||||
messagePositions = [m.start() for m in re.finditer(message, wholeCaption)]
|
messagePositions = [m.start() for m in messageRegex.finditer(wholeCaption)]
|
||||||
if messagePositions != []:
|
if messagePositions != []:
|
||||||
timestamps = []
|
timestamps = []
|
||||||
for messagePosition in messagePositions:
|
for messagePosition in messagePositions:
|
||||||
@@ -67,6 +80,7 @@ for fileIndex, file in enumerate(files):
|
|||||||
if message in str(line):
|
if message in str(line):
|
||||||
write(toWrite)
|
write(toWrite)
|
||||||
break
|
break
|
||||||
|
write(f'progress:{fileIndex + 1} / {len(files)}')
|
||||||
|
|
||||||
with open(clientFilePath) as f:
|
with open(clientFilePath) as f:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
Reference in new issue
Block a user