keyword search, #build
Changed: - added keyword search queries - added fulltext search - authenticate all user created media - reduce docker log output
This commit is contained in:
commit
88b412a2e9
|
@ -3,18 +3,22 @@ server {
|
|||
listen 8000;
|
||||
|
||||
location /cache/videos/ {
|
||||
auth_request /api/ping/;
|
||||
alias /cache/videos/;
|
||||
}
|
||||
|
||||
location /cache/channels/ {
|
||||
auth_request /api/ping/;
|
||||
alias /cache/channels/;
|
||||
}
|
||||
|
||||
location /cache/playlists/ {
|
||||
auth_request /api/ping/;
|
||||
alias /cache/playlists/;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
auth_request /api/ping/;
|
||||
alias /youtube/;
|
||||
types {
|
||||
text/vtt vtt;
|
||||
|
|
|
@ -5,4 +5,7 @@ pidfile = /tmp/project-master.pid
|
|||
vacuum = True
|
||||
max-requests = 5000
|
||||
socket = :8080
|
||||
buffer-size = 8192
|
||||
buffer-size = 8192
|
||||
log-5xx = true
|
||||
log-4xx = true
|
||||
disable-logging = true
|
||||
|
|
|
@ -9,6 +9,7 @@ Table of contents:
|
|||
* [Settings](Settings): All the configuration options
|
||||
* [Video](Video): All details of a single video and playlist navigation.
|
||||
* [Users](Users): User management admin interface
|
||||
* [Search](Search): Search your archive
|
||||
* [Installation](Installation): WIP - detailed installation instructions for various platforms.
|
||||
|
||||
## Getting Started
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# Search Page
|
||||
|
||||
**Functionality described here is not yet in the regular release.**
|
||||
|
||||
Accessible at `/search/` of your **Tube Archivist**, search your archive for Videos, Channels and Playlists - or even full text search throughout your indexed subtitles.
|
||||
|
||||
- All your queries are case insensitive and are normalized to lowercase.
|
||||
- All your queries are analyzed for the english language, this means *singular*, *plural* and word variations like *-ing*, *-ed*, *-able* etc are treated as synonyms.
|
||||
- Fuzzy search is activated for all your searches. This can catch typos in your queries or in the matching documents with one to two letters difference, depending on the query length.
|
||||
- All text searches are ranked, meaning the better a match the higher ranked the result. Unless otherwise stated, queries with multiple words are processed with the `and` operator, meaning all words need to match so each word will narrow down the result.
|
||||
|
||||
Just start typing to start a *simple* search or start your query with a primary keyword to search for a specific type and narrow down the result with secondary keywords. Secondary keywords can be in any order. Use *yes* or *no* for boolean values.
|
||||
|
||||
## Simple
|
||||
Start your query without a keyword to make a simple query. This will search in *video titles*, *channel names* and *playlist titles* and will return matching Videos, Channels and Playlists. Keyword searches will return more results in a particular category due to the fact that more fields are searched for matches.
|
||||
|
||||
## Video
|
||||
Start your query with the primary keyword `video:` to search for videos only. This will search through the *video titles*, *tags* and *category* fields. Narrow your search down with secondary keywords:
|
||||
- `channel:` search for videos matching the channel name.
|
||||
- `active:` is a boolean value, to search for videos that are still active on youtube or that are not active any more.
|
||||
|
||||
**Example**:
|
||||
- `video:learn python channel:corey shafer active:yes`: This will return all videos with the term *Learn Python* from the channel *Corey Shafer* that are still *Active* on YouTube.
|
||||
- `video: channel:tom scott active:no`: Note the omitted term after the primary key, this will show all videos from the channel *Tom Scott* that are no longer active on YouTube.
|
||||
|
||||
## Channel
|
||||
Start with the `channel:` primary keyword to search for channels matching your query. This will search through the *channel name* and *channel description* fields. Narrow your search down with secondary keywords:
|
||||
- `subscribed:` is a boolean value, search for channels that you are subscribed to or not.
|
||||
- `active:` is a boolean value, to search for channels that are still active on YouTube or that are no longer active.
|
||||
|
||||
**Example**:
|
||||
- `channel:linux subscribed:yes`: Search for channels with the term *Linux* that you are subscribed to.
|
||||
- `channel: active:no`: Note the omitted term after the primary key, this will return all channels that are no longer active on YouTube.
|
||||
|
||||
## Playlist
|
||||
Start your query with the primary keyword `playlist:` to search for playlists only. This will search through the *playlist title* and *playlist description* fields. Narrow down your search with these secondary keywords:
|
||||
- `subscribed`: is a boolean value, search for playlists that you are subscribed to or not.
|
||||
- `active:` is a boolean value, to search for playlists that are still active on YouTube or that are no longer active.
|
||||
|
||||
**Example**:
|
||||
- `playlist:backend engineering subscribed:yes`: Search for playlists about *Backend Engineering* that you are subscribed to.
|
||||
- `playlist: active:yes subscribed:yes`: Note the omitted primary search term, this will return all playlists active on YouTube that you are subscribed to.
|
||||
- `playlist:html css active:yes`: Search for playlists containing *HTML CSS* that are still active on YouTube.
|
||||
|
||||
## Full
|
||||
Start a full text search by beginning your query with the primary keyword `full:`. This will search through your indexed Subtitles showing segments with possible matches. This will only show any results if you have activated *subtitle download and index* on the settings page. The operator for full text searches is `or` meaning when searching for multiple words not all words need to match, but additional words will change the ranking of the result, the more words match and the better they match, the higher ranked the result. The matching words will get highlighted in the text preview.
|
||||
|
||||
Clicking the play button on the thumbnail will open the inplace player at the timestamp from where the segment starts. Same when clicking the video title, this will open the video page and put the player at the segment timestamp. This will overwrite any previous playback position.
|
||||
|
||||
Narrow down your search with these secondary keywords:
|
||||
- `lang`: Search for matches only within a language. Use the same two letter ISO country code as you have set on the settings page.
|
||||
- `source`: Can either be *auto* to search through auto generated subtitles only or *user* to search through user uploaded subtitles only.
|
||||
|
||||
**Example**:
|
||||
- `full:contribute to open source lang:en` search for subtitle segments matching with the words *Contribute to Open Source* in the language *en*.
|
||||
- `full:flight simulator cockpit source:user` to search for the words *Flight Simulator Cockpit* from *user* uploaded subtitle segments.
|
|
@ -12,8 +12,8 @@ from home.src.ta.ta_redis import RedisArchivist
|
|||
class StartupCheck:
|
||||
"""checks to run at application startup"""
|
||||
|
||||
MIN_MAJOR, MAX_MAJOR = 7, 7
|
||||
MIN_MINOR = 17
|
||||
MIN_MAJOR, MAX_MAJOR = 8, 8
|
||||
MIN_MINOR = 0
|
||||
|
||||
def __init__(self):
|
||||
self.config_handler = ArchivistConfig()
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
"app_root": "/app",
|
||||
"cache_dir": "/cache",
|
||||
"videos": "/youtube",
|
||||
"file_template": "%(id)s_%(title)s.mp4",
|
||||
"colors": "dark",
|
||||
"enable_cast": false
|
||||
},
|
||||
|
|
|
@ -241,12 +241,8 @@ class VideoDownloader:
|
|||
|
||||
def _progress_hook(self, response):
|
||||
"""process the progress_hooks from yt_dlp"""
|
||||
# title
|
||||
path = os.path.split(response["filename"])[-1][12:]
|
||||
filename = os.path.splitext(os.path.splitext(path)[0])[0]
|
||||
filename_clean = filename.replace("_", " ")
|
||||
title = "Downloading: " + filename_clean
|
||||
# message
|
||||
title = "Downloading: " + response["info_dict"]["title"]
|
||||
|
||||
try:
|
||||
percent = response["_percent_str"]
|
||||
size = response["_total_bytes_str"]
|
||||
|
@ -255,6 +251,7 @@ class VideoDownloader:
|
|||
message = f"{percent} of {size} at {speed} - time left: {eta}"
|
||||
except KeyError:
|
||||
message = "processing"
|
||||
|
||||
mess_dict = {
|
||||
"status": self.MSG,
|
||||
"level": "info",
|
||||
|
@ -274,11 +271,9 @@ class VideoDownloader:
|
|||
self.obs = {
|
||||
"default_search": "ytsearch",
|
||||
"merge_output_format": "mp4",
|
||||
"restrictfilenames": True,
|
||||
"outtmpl": (
|
||||
self.config["application"]["cache_dir"]
|
||||
+ "/download/"
|
||||
+ self.config["application"]["file_template"]
|
||||
+ "/download/%(id)s.mp4"
|
||||
),
|
||||
"progress_hooks": [self._progress_hook],
|
||||
"noprogress": True,
|
||||
|
|
|
@ -136,6 +136,11 @@ class SearchHandler:
|
|||
date_str = datetime.strftime(date_refresh, "%d %b, %Y")
|
||||
hit["source"]["channel"]["channel_last_refresh"] = date_str
|
||||
|
||||
if "subtitle_fragment_id" in hit_keys:
|
||||
youtube_id = hit["source"]["youtube_id"]
|
||||
thumb_path = ThumbManager().vid_thumb_path(youtube_id)
|
||||
hit["source"]["vid_thumb_url"] = f"/cache/{thumb_path}"
|
||||
|
||||
return hit
|
||||
|
||||
|
||||
|
@ -146,38 +151,12 @@ class SearchForm:
|
|||
|
||||
def multi_search(self, search_query):
|
||||
"""searching through index"""
|
||||
path = "ta_video,ta_channel,ta_playlist/_search"
|
||||
data = {
|
||||
"size": 30,
|
||||
"query": {
|
||||
"multi_match": {
|
||||
"query": search_query,
|
||||
"type": "bool_prefix",
|
||||
"operator": "and",
|
||||
"fuzziness": "auto",
|
||||
"fields": [
|
||||
"category",
|
||||
"channel_description",
|
||||
"channel_name._2gram",
|
||||
"channel_name._3gram",
|
||||
"channel_name.search_as_you_type",
|
||||
"playlist_description",
|
||||
"playlist_name._2gram",
|
||||
"playlist_name._3gram",
|
||||
"playlist_name.search_as_you_type",
|
||||
"tags",
|
||||
"title._2gram",
|
||||
"title._3gram",
|
||||
"title.search_as_you_type",
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
look_up = SearchHandler(path, config=self.CONFIG, data=data)
|
||||
path, query, query_type = SearchParser(search_query).run()
|
||||
look_up = SearchHandler(path, config=self.CONFIG, data=query)
|
||||
search_results = look_up.get_data()
|
||||
all_results = self.build_results(search_results)
|
||||
|
||||
return {"results": all_results}
|
||||
return {"results": all_results, "queryType": query_type}
|
||||
|
||||
@staticmethod
|
||||
def build_results(search_results):
|
||||
|
@ -185,6 +164,7 @@ class SearchForm:
|
|||
video_results = []
|
||||
channel_results = []
|
||||
playlist_results = []
|
||||
fulltext_results = []
|
||||
if search_results:
|
||||
for result in search_results:
|
||||
if result["_index"] == "ta_video":
|
||||
|
@ -193,11 +173,337 @@ class SearchForm:
|
|||
channel_results.append(result)
|
||||
elif result["_index"] == "ta_playlist":
|
||||
playlist_results.append(result)
|
||||
elif result["_index"] == "ta_subtitle":
|
||||
fulltext_results.append(result)
|
||||
|
||||
all_results = {
|
||||
"video_results": video_results,
|
||||
"channel_results": channel_results,
|
||||
"playlist_results": playlist_results,
|
||||
"fulltext_results": fulltext_results,
|
||||
}
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
class SearchParser:
|
||||
"""handle structured searches"""
|
||||
|
||||
def __init__(self, search_query):
|
||||
self.query_words = search_query.lower().split()
|
||||
self.query_map = False
|
||||
self.append_to = "term"
|
||||
|
||||
def run(self):
|
||||
"""collection, return path and query dict for es"""
|
||||
print(f"query words: {self.query_words}")
|
||||
query_type = self._find_map()
|
||||
self._run_words()
|
||||
self._delete_unset()
|
||||
self._match_data_types()
|
||||
|
||||
path, query = QueryBuilder(self.query_map, query_type).run()
|
||||
|
||||
return path, query, query_type
|
||||
|
||||
def _find_map(self):
|
||||
"""find query in keyword map"""
|
||||
first_word = self.query_words[0]
|
||||
key_word_map = self._get_map()
|
||||
|
||||
if ":" in first_word:
|
||||
index_match, query_string = first_word.split(":")
|
||||
if index_match in key_word_map:
|
||||
self.query_map = key_word_map.get(index_match)
|
||||
self.query_words[0] = query_string
|
||||
return index_match
|
||||
|
||||
self.query_map = key_word_map.get("simple")
|
||||
print(f"query_map: {self.query_map}")
|
||||
|
||||
return "simple"
|
||||
|
||||
@staticmethod
|
||||
def _get_map():
|
||||
"""return map to build on"""
|
||||
return {
|
||||
"simple": {
|
||||
"index": "ta_video,ta_channel,ta_playlist",
|
||||
"term": [],
|
||||
},
|
||||
"video": {
|
||||
"index": "ta_video",
|
||||
"term": [],
|
||||
"channel": [],
|
||||
"active": [],
|
||||
},
|
||||
"channel": {
|
||||
"index": "ta_channel",
|
||||
"term": [],
|
||||
"active": [],
|
||||
"subscribed": [],
|
||||
},
|
||||
"playlist": {
|
||||
"index": "ta_playlist",
|
||||
"term": [],
|
||||
"active": [],
|
||||
"subscribed": [],
|
||||
},
|
||||
"full": {
|
||||
"index": "ta_subtitle",
|
||||
"term": [],
|
||||
"lang": [],
|
||||
"source": [],
|
||||
},
|
||||
}
|
||||
|
||||
def _run_words(self):
|
||||
"""append word by word"""
|
||||
for word in self.query_words:
|
||||
if ":" in word:
|
||||
keyword, search_string = word.split(":")
|
||||
if keyword in self.query_map:
|
||||
self.append_to = keyword
|
||||
word = search_string
|
||||
|
||||
if word:
|
||||
self.query_map[self.append_to].append(word)
|
||||
|
||||
def _delete_unset(self):
|
||||
"""delete unset keys"""
|
||||
new_query_map = {}
|
||||
for key, value in self.query_map.items():
|
||||
if value:
|
||||
new_query_map.update({key: value})
|
||||
self.query_map = new_query_map
|
||||
|
||||
def _match_data_types(self):
|
||||
"""match values with data types"""
|
||||
for key, value in self.query_map.items():
|
||||
if key in ["term", "channel"]:
|
||||
self.query_map[key] = " ".join(self.query_map[key])
|
||||
if key in ["active", "subscribed"]:
|
||||
self.query_map[key] = "yes" in value
|
||||
|
||||
|
||||
class QueryBuilder:
|
||||
"""build query for ES from form data"""
|
||||
|
||||
def __init__(self, query_map, query_type):
|
||||
self.query_map = query_map
|
||||
self.query_type = query_type
|
||||
|
||||
def run(self):
|
||||
"""build query"""
|
||||
path = self._build_path()
|
||||
query = self.build_query()
|
||||
print(f"es path: {path}")
|
||||
print(f"query: {query}")
|
||||
|
||||
return path, query
|
||||
|
||||
def _build_path(self):
|
||||
"""build es index search path"""
|
||||
return f"{self.query_map.get('index')}/_search"
|
||||
|
||||
def build_query(self):
|
||||
"""build query based on query_type"""
|
||||
|
||||
exec_map = {
|
||||
"simple": self._build_simple,
|
||||
"video": self._build_video,
|
||||
"channel": self._build_channel,
|
||||
"playlist": self._build_playlist,
|
||||
"full": self._build_fulltext,
|
||||
}
|
||||
|
||||
build_must_list = exec_map[self.query_type]
|
||||
|
||||
if self.query_type == "full":
|
||||
query = build_must_list()
|
||||
else:
|
||||
query = {
|
||||
"size": 30,
|
||||
"query": {"bool": {"must": build_must_list()}},
|
||||
}
|
||||
|
||||
return query
|
||||
|
||||
def _build_simple(self):
|
||||
"""build simple cross index query"""
|
||||
must_list = []
|
||||
|
||||
if (term := self.query_map.get("term")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": term,
|
||||
"type": "bool_prefix",
|
||||
"fuzziness": "auto",
|
||||
"operator": "and",
|
||||
"fields": [
|
||||
"channel_name._2gram",
|
||||
"channel_name._3gram",
|
||||
"channel_name.search_as_you_type",
|
||||
"playlist_name._2gram",
|
||||
"playlist_name._3gram",
|
||||
"playlist_name.search_as_you_type",
|
||||
"title._2gram",
|
||||
"title._3gram",
|
||||
"title.search_as_you_type",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return must_list
|
||||
|
||||
def _build_video(self):
|
||||
"""build video query"""
|
||||
must_list = []
|
||||
|
||||
if (term := self.query_map.get("term")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": term,
|
||||
"type": "bool_prefix",
|
||||
"fuzziness": "auto",
|
||||
"operator": "and",
|
||||
"fields": [
|
||||
"title._2gram^2",
|
||||
"title._3gram^2",
|
||||
"title.search_as_you_type^2",
|
||||
"tags",
|
||||
"category",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (active := self.query_map.get("active")) is not None:
|
||||
must_list.append({"term": {"active": {"value": active}}})
|
||||
|
||||
if (channel := self.query_map.get("channel")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": channel,
|
||||
"type": "bool_prefix",
|
||||
"fuzziness": "auto",
|
||||
"operator": "and",
|
||||
"fields": [
|
||||
"channel.channel_name._2gram",
|
||||
"channel.channel_name._3gram",
|
||||
"channel.channel_name.search_as_you_type",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return must_list
|
||||
|
||||
def _build_channel(self):
|
||||
"""build query for channel"""
|
||||
must_list = []
|
||||
|
||||
if (term := self.query_map.get("term")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": term,
|
||||
"type": "bool_prefix",
|
||||
"fuzziness": "auto",
|
||||
"operator": "and",
|
||||
"fields": [
|
||||
"channel_description",
|
||||
"channel_name._2gram^2",
|
||||
"channel_name._3gram^2",
|
||||
"channel_name.search_as_you_type^2",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (active := self.query_map.get("active")) is not None:
|
||||
must_list.append({"term": {"channel_active": {"value": active}}})
|
||||
|
||||
if (subscribed := self.query_map.get("subscribed")) is not None:
|
||||
must_list.append(
|
||||
{"term": {"channel_subscribed": {"value": subscribed}}}
|
||||
)
|
||||
|
||||
return must_list
|
||||
|
||||
def _build_playlist(self):
|
||||
"""build query for playlist"""
|
||||
must_list = []
|
||||
|
||||
if (term := self.query_map.get("term")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"multi_match": {
|
||||
"query": term,
|
||||
"type": "bool_prefix",
|
||||
"fuzziness": "auto",
|
||||
"operator": "and",
|
||||
"fields": [
|
||||
"playlist_description",
|
||||
"playlist_name._2gram^2",
|
||||
"playlist_name._3gram^2",
|
||||
"playlist_name.search_as_you_type^2",
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (active := self.query_map.get("active")) is not None:
|
||||
must_list.append({"term": {"playlist_active": {"value": active}}})
|
||||
|
||||
if (subscribed := self.query_map.get("subscribed")) is not None:
|
||||
must_list.append(
|
||||
{"term": {"playlist_subscribed": {"value": subscribed}}}
|
||||
)
|
||||
|
||||
return must_list
|
||||
|
||||
def _build_fulltext(self):
|
||||
"""build query for fulltext search"""
|
||||
must_list = []
|
||||
|
||||
if (term := self.query_map.get("term")) is not None:
|
||||
must_list.append(
|
||||
{
|
||||
"match": {
|
||||
"subtitle_line": {
|
||||
"query": term,
|
||||
"fuzziness": "auto",
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (lang := self.query_map.get("lang")) is not None:
|
||||
must_list.append({"term": {"subtitle_lang": {"value": lang[0]}}})
|
||||
|
||||
if (source := self.query_map.get("source")) is not None:
|
||||
must_list.append(
|
||||
{"term": {"subtitle_source": {"value": source[0]}}}
|
||||
)
|
||||
|
||||
query = {
|
||||
"size": 30,
|
||||
"_source": {"excludes": "subtitle_line"},
|
||||
"query": {"bool": {"must": must_list}},
|
||||
"highlight": {
|
||||
"fields": {
|
||||
"subtitle_line": {
|
||||
"number_of_fragments": 0,
|
||||
"pre_tags": ['<span class="settings-current">'],
|
||||
"post_tags": ["</span>"],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return query
|
||||
|
|
|
@ -105,6 +105,18 @@ def date_praser(timestamp):
|
|||
return datetime.strftime(date_obj, "%d %b, %Y")
|
||||
|
||||
|
||||
def time_parser(timestamp):
|
||||
"""return seconds from timestamp, false on empty"""
|
||||
if not timestamp:
|
||||
return False
|
||||
|
||||
if timestamp.isnumeric():
|
||||
return int(timestamp)
|
||||
|
||||
hours, minutes, seconds = timestamp.split(":", maxsplit=3)
|
||||
return int(hours) * 60 * 60 + int(minutes) * 60 + float(seconds)
|
||||
|
||||
|
||||
class UrlListParser:
|
||||
"""take a multi line string and detect valid youtube ids"""
|
||||
|
||||
|
|
|
@ -26,5 +26,11 @@
|
|||
<p>No playlists found.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multi-search-result">
|
||||
<h2>Fulltext Results</h2>
|
||||
<div id="fulltext-results" class="video-list list">
|
||||
<p>No fulltext results found.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -125,7 +125,11 @@
|
|||
<script>
|
||||
var videoData = getVideoData('{{ video.youtube_id }}');
|
||||
sponsorBlock = videoData.data.sponsorblock;
|
||||
var videoProgress = getVideoProgress('{{ video.youtube_id }}').position;
|
||||
{% if position %}
|
||||
var videoProgress = {{ position }}
|
||||
{% else %}
|
||||
var videoProgress = getVideoProgress('{{ video.youtube_id }}').position;
|
||||
{% endif %}
|
||||
window.onload = insertVideoTag(videoData, videoProgress);
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -35,7 +35,7 @@ from home.src.index.channel import channel_overwrites
|
|||
from home.src.index.generic import Pagination
|
||||
from home.src.index.playlist import YoutubePlaylist
|
||||
from home.src.ta.config import AppConfig, ScheduleBuilder
|
||||
from home.src.ta.helper import UrlListParser
|
||||
from home.src.ta.helper import UrlListParser, time_parser
|
||||
from home.src.ta.ta_redis import RedisArchivist
|
||||
from home.tasks import extrac_dl, index_channel_playlists, subscribe_to
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
@ -771,6 +771,7 @@ class VideoView(View):
|
|||
def get(self, request, video_id):
|
||||
"""get single video"""
|
||||
config_handler = AppConfig(request.user.id)
|
||||
position = time_parser(request.GET.get("t"))
|
||||
path = f"ta_video/_doc/{video_id}"
|
||||
look_up = SearchHandler(path, config=False)
|
||||
video_hit = look_up.get_data()
|
||||
|
@ -796,6 +797,7 @@ class VideoView(View):
|
|||
"cast": config_handler.config["application"]["enable_cast"],
|
||||
"version": settings.TA_VERSION,
|
||||
"config": config_handler.config,
|
||||
"position": position,
|
||||
}
|
||||
return render(request, "home/video.html", context)
|
||||
|
||||
|
|
|
@ -9,4 +9,4 @@ requests==2.28.1
|
|||
ryd-client==0.0.3
|
||||
uWSGI==2.0.20
|
||||
whitenoise==6.2.0
|
||||
yt_dlp==2022.6.29
|
||||
yt_dlp==2022.7.18
|
||||
|
|
|
@ -452,7 +452,7 @@ button:hover {
|
|||
|
||||
.video-item.list {
|
||||
display: grid;
|
||||
grid-template-columns: 25% auto;
|
||||
grid-template-columns: 28% auto;
|
||||
}
|
||||
|
||||
.video-progress-bar {
|
||||
|
@ -781,6 +781,10 @@ button:hover {
|
|||
}
|
||||
|
||||
/* multi search page */
|
||||
.multi-search-box {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.multi-search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -336,10 +336,18 @@ function cancelDelete() {
|
|||
document.getElementById("delete-item").style.display = 'block';
|
||||
}
|
||||
|
||||
// get seconds from hh:mm:ss.ms timestamp
|
||||
function getSeconds(timestamp) {
|
||||
var elements = timestamp.split(":", 3);
|
||||
var secs = parseInt(elements[0]) * 60 * 60 + parseInt(elements[1]) * 60 + parseFloat(elements[2])
|
||||
return secs
|
||||
}
|
||||
|
||||
// player
|
||||
var sponsorBlock = [];
|
||||
function createPlayer(button) {
|
||||
var videoId = button.getAttribute('data-id');
|
||||
var videoPosition = button.getAttribute('data-position');
|
||||
var videoData = getVideoData(videoId);
|
||||
|
||||
var sponsorBlockElements = '';
|
||||
|
@ -363,8 +371,11 @@ function createPlayer(button) {
|
|||
} else {
|
||||
sponsorBlock = null;
|
||||
}
|
||||
|
||||
var videoProgress = getVideoProgress(videoId).position;
|
||||
if (videoPosition) {
|
||||
var videoProgress = getSeconds(videoPosition)
|
||||
} else {
|
||||
var videoProgress = getVideoProgress(videoId).position;
|
||||
}
|
||||
var videoName = videoData.data.title;
|
||||
|
||||
var videoTag = createVideoTag(videoData, videoProgress);
|
||||
|
@ -761,7 +772,9 @@ function removePlayer() {
|
|||
playerElement.innerHTML = '';
|
||||
// append played status
|
||||
var videoInfo = document.getElementById('video-info-' + youtubeId);
|
||||
videoInfo.insertBefore(playedStatus, videoInfo.firstChild);
|
||||
if (videoInfo) {
|
||||
videoInfo.insertBefore(playedStatus, videoInfo.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -794,8 +807,8 @@ function searchMulti(query) {
|
|||
var http = new XMLHttpRequest();
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState === 4) {
|
||||
allResults = JSON.parse(http.response).results;
|
||||
populateMultiSearchResults(allResults);
|
||||
response = JSON.parse(http.response);
|
||||
populateMultiSearchResults(response.results, response.queryType);
|
||||
}
|
||||
};
|
||||
http.open("POST", "/process/", true);
|
||||
|
@ -811,36 +824,83 @@ function getViewDefaults(view) {
|
|||
return defaultView;
|
||||
}
|
||||
|
||||
function populateMultiSearchResults(allResults) {
|
||||
function populateMultiSearchResults(allResults, queryType) {
|
||||
// videos
|
||||
var defaultVideo = getViewDefaults("home");
|
||||
var allVideos = allResults.video_results;
|
||||
var videoBox = document.getElementById("video-results");
|
||||
videoBox.innerHTML = "";
|
||||
for (let index = 0; index < allVideos.length; index++) {
|
||||
const video = allVideos[index].source;
|
||||
const videoDiv = createVideo(video, defaultVideo);
|
||||
videoBox.appendChild(videoDiv);
|
||||
videoBox.parentElement.style.display = "block";
|
||||
if (allVideos.length > 0) {
|
||||
for (let index = 0; index < allVideos.length; index++) {
|
||||
const video = allVideos[index].source;
|
||||
const videoDiv = createVideo(video, defaultVideo);
|
||||
videoBox.appendChild(videoDiv);
|
||||
}
|
||||
} else {
|
||||
if (queryType === "simple" || queryType == "video") {
|
||||
videoBox.innerHTML = "<p>No videos found.</p>";
|
||||
} else {
|
||||
videoBox.parentElement.style.display = "none";
|
||||
}
|
||||
}
|
||||
// channels
|
||||
var defaultChannel = getViewDefaults("channel");
|
||||
var allChannels = allResults.channel_results;
|
||||
var channelBox = document.getElementById("channel-results");
|
||||
channelBox.innerHTML = "";
|
||||
for (let index = 0; index < allChannels.length; index++) {
|
||||
const channel = allChannels[index].source;
|
||||
const channelDiv = createChannel(channel, defaultChannel);
|
||||
channelBox.appendChild(channelDiv);
|
||||
channelBox.parentElement.style.display = "block";
|
||||
if (allChannels.length > 0) {
|
||||
for (let index = 0; index < allChannels.length; index++) {
|
||||
const channel = allChannels[index].source;
|
||||
const channelDiv = createChannel(channel, defaultChannel);
|
||||
channelBox.appendChild(channelDiv);
|
||||
}
|
||||
} else {
|
||||
if (queryType === "simple" || queryType == "channel") {
|
||||
channelBox.innerHTML = "<p>No channels found.</p>";
|
||||
} else {
|
||||
channelBox.parentElement.style.display = "none";
|
||||
}
|
||||
}
|
||||
// playlists
|
||||
var defaultPlaylist = getViewDefaults("playlist");
|
||||
var allPlaylists = allResults.playlist_results;
|
||||
var playlistBox = document.getElementById("playlist-results");
|
||||
playlistBox.innerHTML = "";
|
||||
for (let index = 0; index < allPlaylists.length; index++) {
|
||||
const playlist = allPlaylists[index].source;
|
||||
const playlistDiv = createPlaylist(playlist, defaultPlaylist);
|
||||
playlistBox.appendChild(playlistDiv);
|
||||
playlistBox.parentElement.style.display = "block";
|
||||
if (allPlaylists.length > 0) {
|
||||
for (let index = 0; index < allPlaylists.length; index++) {
|
||||
const playlist = allPlaylists[index].source;
|
||||
const playlistDiv = createPlaylist(playlist, defaultPlaylist);
|
||||
playlistBox.appendChild(playlistDiv);
|
||||
}
|
||||
} else {
|
||||
if (queryType === "simple" || queryType == "playlist") {
|
||||
playlistBox.innerHTML = "<p>No playlists found.</p>";
|
||||
} else {
|
||||
playlistBox.parentElement.style.display = "none";
|
||||
}
|
||||
}
|
||||
// fulltext
|
||||
var allFullText = allResults.fulltext_results;
|
||||
var fullTextBox = document.getElementById("fulltext-results");
|
||||
fullTextBox.innerHTML = "";
|
||||
fullTextBox.parentElement.style.display = "block";
|
||||
if (allFullText.length > 0) {
|
||||
for (let i = 0; i < allFullText.length; i++) {
|
||||
const fullText = allFullText[i];
|
||||
if ("highlight" in fullText) {
|
||||
const fullTextDiv = createFulltext(fullText);
|
||||
fullTextBox.appendChild(fullTextDiv);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (queryType === "simple" || queryType == "full") {
|
||||
fullTextBox.innerHTML = "<p>No fulltext items found.</p>";
|
||||
} else {
|
||||
fullTextBox.parentElement.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -862,7 +922,7 @@ function createVideo(video, viewStyle) {
|
|||
const channelName = video.channel.channel_name;
|
||||
// build markup
|
||||
const markup = `
|
||||
<a href="#player" data-src="/media/${mediaUrl}" data-thumb="${thumbUrl}" data-title="${videoTitle}" data-channel="${channelName}" data-channel-id="${channelId}" data-id="${videoId}" onclick="createPlayer(this)">
|
||||
<a href="#player" data-id="${videoId}" onclick="createPlayer(this)">
|
||||
<div class="video-thumb-wrap ${viewStyle}">
|
||||
<div class="video-thumb">
|
||||
<img src="${thumbUrl}" alt="video-thumb">
|
||||
|
@ -965,6 +1025,40 @@ function createPlaylist(playlist, viewStyle) {
|
|||
return playlistDiv;
|
||||
}
|
||||
|
||||
function createFulltext(fullText) {
|
||||
const videoId = fullText.source.youtube_id;
|
||||
const videoTitle = fullText.source.title;
|
||||
const thumbUrl = fullText.source.vid_thumb_url;
|
||||
const channelId = fullText.source.subtitle_channel_id;
|
||||
const channelName = fullText.source.subtitle_channel;
|
||||
const subtitleLine = fullText.highlight.subtitle_line[0];
|
||||
const subtitle_start = fullText.source.subtitle_start.split(".")[0];
|
||||
const subtitle_end = fullText.source.subtitle_end.split(".")[0];
|
||||
const markup = `
|
||||
<a href="#player" data-id="${videoId}" data-position="${subtitle_start}" onclick="createPlayer(this)">
|
||||
<div class="video-thumb-wrap list">
|
||||
<div class="video-thumb">
|
||||
<img src="${thumbUrl}" alt="video-thumb">
|
||||
</div>
|
||||
<div class="video-play">
|
||||
<img src="/static/img/icon-play.svg" alt="play-icon">
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="video-desc list">
|
||||
<p>${subtitle_start} - ${subtitle_end}</p>
|
||||
<p>${subtitleLine}</p>
|
||||
<div>
|
||||
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
|
||||
<a class="video-more" href="/video/${videoId}/?t=${subtitle_start}"><h2>${videoTitle}</h2></a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const fullTextDiv = document.createElement("div");
|
||||
fullTextDiv.setAttribute("class", "video-item list");
|
||||
fullTextDiv.innerHTML = markup;
|
||||
return fullTextDiv
|
||||
}
|
||||
|
||||
// generic
|
||||
|
||||
|
|
Loading…
Reference in New Issue