1
0
Fork 0
mirror of https://github.com/Findus23/cr-search.git synced 2024-09-19 15:23:44 +02:00

add one-shot support and lots of additional series

This commit is contained in:
Lukas Winkler 2021-07-04 22:24:51 +02:00
parent ff2a017d44
commit 20865d73fe
Signed by: lukas
GPG key ID: 54DE4D798D244853
53 changed files with 29590 additions and 8862 deletions

174
data.py
View file

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Optional
from typing import Optional, List
colors_c1 = {
"Travis": "#7592a4",
@ -79,7 +79,9 @@ assert set(single_speaker["Handbooker Helper"].keys()) == set(range(1, 44 + 1))
@dataclass
class SeriesData:
name: str
playlist_id: str
slug: str
playlist_id: Optional[str] = None
videos: Optional[List[str]] = None
single_speaker: bool = False
initial_speaker: Optional[str] = None
@ -87,29 +89,197 @@ class SeriesData:
series_data = [
SeriesData(
name="Campaign 1",
slug="campaign1",
playlist_id="PL1tiwbzkOjQz7D0l_eLJGAISVtcL7oRu_"
),
SeriesData(
name="Campaign 2",
slug="campaign2",
playlist_id="PL1tiwbzkOjQxD0jjAE7PsWoaCrs0EkBH2"
),
SeriesData(
name="Handbooker Helper",
slug="HandbookerHelper",
playlist_id="PL1tiwbzkOjQyr6-gqJ8r29j_rJkR49uDN",
single_speaker=True
),
SeriesData(
name="Mini Primetime",
slug="MiniPrimetime",
playlist_id="PL1tiwbzkOjQz9kKDaPRPrX2E7RPTaxEZd",
initial_speaker="Will"
),
SeriesData(
name="The Legend of The Legend of Vox Machina",
slug="TheLegendofTheLegendofVoxMachina",
playlist_id="PL1tiwbzkOjQwJdoNetaNJE1zZVOE7xi8u"
),
SeriesData(
name="Crit Recap Animated",
slug="CritRecapAnimated",
playlist_id="PL1tiwbzkOjQy8yF8esjgVomDXKD-XmG20",
initial_speaker="?"
),
SeriesData(
name="Exandria Unlimited",
slug="ExandriaUnlimited",
playlist_id="PL1tiwbzkOjQzSnYHVT8X4pyMIbSX3i4gz"
),
SeriesData(
name="Critter Hug",
slug="CritterHug",
playlist_id="PL1tiwbzkOjQw6CxZVgtRsY_0WqK5DsNE1",
single_speaker=True # no names in subtitles
),
SeriesData(
name="UnDeadwood",
slug="UnDeadwood",
# playlist_id="PL1tiwbzkOjQwuwLkGnqVdJnzQ-YNX2_qz"
videos=["AEIGOY6WDoA", "JlAW2qeLsL0", "jSGw5L9xds0", "WHxuuQ-P2Cg"]
),
SeriesData(
name="The Adventures of the Darrington Brigade",
slug="darringtonBrigade",
videos=["pVu_Ib1fpVI"]
),
SeriesData(
name="Dalen's Closet",
slug="DalensCloset",
videos=["0oclW3MXABA"]
),
SeriesData(
name="Call of Cthulhu: Shadow of the Crystal Palace",
slug="CallOfCthulhu",
videos=["0uhqZdJ8swQ"],
single_speaker=True # no names in subtitles
),
SeriesData(
name="The Search For Bob",
slug="TheSearchForBob",
videos=["AfEZF5G9HV4"]
),
# Tails of Equestria One-Shot
SeriesData(
name="Stephen Colbert's D&D Adventure with Matthew Mercer",
slug="StephenColbertOneShot",
videos=["3658C2y4LlA"],
single_speaker=True # no names in subtitles
),
SeriesData(
name="The Search For Grog",
slug="TheSearchForGrog",
videos=["hi5pEHs76TE"]
),
SeriesData(
name="The Night Before Critmas",
slug="TheNightBeforeCritmas",
videos=["8zxeGydXY98"],
single_speaker=True # no names in subtitles
),
SeriesData(
name="Honey Heist",
slug="HoneyHeist",
videos=["9jbGshiuFs4", "MSNK4ThPHqc", "whbc64O0Yik"]
),
SeriesData(
name="Epic Level Battle Royale One-Shot",
slug="EpicLevelBattleRoyaleOneShot",
videos=["q3BGg0d8DvU"]
),
SeriesData(
name="Sam's One-Shot",
slug="SamsOneShot",
videos=["LfeAYN8f1AU"]
),
SeriesData(
name="Hearthstone One-Shot",
slug="HearthstoneOneShot",
videos=["qA4-q4gk_yY"]
),
SeriesData(
name="Grog's One-Shot",
slug="GrogsOneShot",
videos=["kLnvrocetq8"]
),
SeriesData(
name="Bar Room Blitz",
slug="BarRoomBlitz",
videos=["rnq3VBQu_kI"]
),
SeriesData(
name="Thursday By Night",
slug="ThursdayByNight",
videos=["rnq3VBQu_kI", "eXPu1wk-Ev4"]
),
SeriesData(
name="Shadow of War",
slug="ShadowofWar",
videos=["c9lC5_qjkFE", "Mk21j54rX-M"]
),
SeriesData(
name="Battle Royale One-Shot",
slug="BattleRoyaleOneShot",
videos=["tasz1xUVLhg"]
),
SeriesData(
name="Liam's Quest: Full Circle",
slug="LiamsQuestFullCircle",
videos=["LHita2t54xY"]
),
SeriesData(
name="The Return of Liam!",
slug="TheReturnofLiam",
videos=["LgHm3Ct0Zh0"]
),
SeriesData(
name="Show Q&A and Battle Royale",
slug="ShowQnAandBattleRoyale",
videos=["4FI8qB-yh-w"]
),
SeriesData(
name="Liam's Quest!",
slug="LiamsQuest",
videos=["7Tdl6GhiSI8"]
),
SeriesData(
name="Deadlands One-Shot",
slug="DeadlandsOneShot",
videos=["q0hjGf2bK08"]
),
SeriesData(
name="TO THE POOP! - The Goblins",
slug="ToThePoop",
videos=["u8MRyyFDX3c"]
),
SeriesData(
name="Critical Trolls",
slug="CriticalTrolls",
videos=["EjimabBvZgw"]
),
SeriesData(
name="Cinderbrush: A Monsterhearts Story",
slug="Cinderbrush",
videos=["51ykIVq9KcM"]
),
SeriesData(
name="DOOM Eternal One-Shot",
slug="DOOMEternalOneShot",
videos=["CX8I4M7MPo4"]
),
SeriesData(
name="Diablo One Shot",
slug="DiabloOneShot",
videos=["yODMT1m85FQ"]
),
SeriesData(
name="The Elder Scrolls Online: Blackwood",
slug="TheElderScrollsOnline",
videos=["E-YCzpYDIyA"]
),
SeriesData(
name="Vox Machina vs. Mighty Nein",
slug="VoxMachinaVsMightyNein",
videos=["LpBIQhWAhuM"]
)
]

View file

@ -1,9 +1,12 @@
import argparse
import hashlib
import os
from datetime import datetime
from pathlib import Path
from shutil import move
from subprocess import run
import requests
import youtube_dl
from peewee import DoesNotExist
@ -11,8 +14,10 @@ from data import series_data
from models import Episode, Series, Line, Phrase
from utils import srtdir, pretty_title, title_to_episodenumber
static_path = Path("static")
def main() -> None:
def main(args) -> None:
os.nice(15)
for series in series_data:
name = series.name
@ -26,42 +31,53 @@ def main() -> None:
s.is_campaign = is_campaign
s.single_speaker = series.single_speaker
s.slug = series.slug
s.save()
ydl_opts = {
'extract_flat': True
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
playlist = ydl.extract_info("https://www.youtube.com/playlist?list=" + playlist_id, download=False)
videos = playlist["entries"]
print(v["url"] for v in videos)
if series.playlist_id:
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
playlist = ydl.extract_info("https://www.youtube.com/playlist?list=" + playlist_id, download=False)
videos = playlist["entries"]
urls = [v["url"] for v in videos]
else:
urls = series.videos
ydl_opts_download = {
"writesubtitles": True,
"subtitleslangs": ["en", "en-US"],
"skip_download": True,
}
for nr, video in enumerate(videos, 1):
for nr, url in enumerate(urls, 1):
try:
e = Episode.select().where((Episode.series == s) & (Episode.video_number == nr)).get()
# if (e.series.id == 1) or (e.series.id == 2 and e.video_number < 90):
# continue
# if e.downloaded:
# continue
e = Episode.select().where((Episode.youtube_id == url)).get()
if args.skip_existing and e.downloaded:
continue
except DoesNotExist:
e = Episode()
e.series = s
e.video_number = nr
e.title = video["title"]
e.pretty_title = pretty_title(video["title"])
if s.is_campaign:
if e.series.id == 1 and ("Search For Grog" in e.title or "Search For Bob" in e.title):
e.youtube_id = url
video_info = ydl.extract_info(f'https://www.youtube.com/watch?v={e.youtube_id}', download=False)
if nr == 1:
file = static_path / f"{s.slug}.webp"
if file.exists():
continue
r = requests.get(f"https://i.ytimg.com/vi_webp/{e.youtube_id}/maxresdefault.webp")
r.raise_for_status()
with file.open("wb")as f:
f.write(r.content)
e.upload_date = datetime.strptime(video_info["upload_date"], "%Y%m%d")
e.title = video_info["title"]
e.pretty_title = pretty_title(video_info["title"])
if s.is_campaign or "Exandria" in e.title:
if e.series.id == 1 and ("One-Shot" in e.title or "Search For Bob" in e.title):
continue
e.episode_number = title_to_episodenumber(e.title, e.video_number)
else:
e.episode_number = e.video_number
e.youtube_id = video["url"]
e.save()
print(e.series.id, e.episode_number, e.pretty_title)
@ -100,4 +116,8 @@ def main() -> None:
if __name__ == '__main__':
main()
parser = argparse.ArgumentParser(description="fetch episode data from YouTube")
parser.add_argument("--skip-existing", dest="skip_existing", action="store_true",
help="don't check for update on existing videos")
args = parser.parse_args()
main(args)

View file

@ -1,6 +1,7 @@
from datetime import datetime
from peewee import PostgresqlDatabase, Model, IntegerField, CharField, BooleanField, ForeignKeyField, DateTimeField
from peewee import PostgresqlDatabase, Model, IntegerField, CharField, BooleanField, ForeignKeyField, DateTimeField, \
DateField
from playhouse.postgres_ext import TSVectorField
from config import dbauth
@ -16,12 +17,16 @@ class BaseModel(Model):
class Series(BaseModel):
title = CharField(max_length=100)
slug = CharField(max_length=100)
is_campaign = BooleanField()
single_speaker = BooleanField()
def __str__(self):
return f"<Series: {self.title}>"
class Episode(BaseModel):
series = ForeignKeyField(Series, backref="episodes")
series = ForeignKeyField(Series, backref="episodes", on_delete="CASCADE")
episode_number = IntegerField()
video_number = IntegerField()
youtube_id = CharField(max_length=11)
@ -31,6 +36,7 @@ class Episode(BaseModel):
text_imported = BooleanField(default=False)
phrases_imported = BooleanField(default=False)
subtitle_hash = CharField(max_length=64, null=True)
upload_date = DateField()
last_updated = DateTimeField(default=datetime.now)
class Meta:
@ -40,6 +46,9 @@ class Episode(BaseModel):
def name(self) -> str:
return f"C{self.season}E{self.episode_number:03d}"
def __str__(self):
return f"<Episode: {self.title}>"
class Person(BaseModel):
name = CharField()
@ -49,26 +58,35 @@ class Person(BaseModel):
class Meta:
indexes = ((("name", "series"), True),)
def __str__(self):
return f"<Person: {self.name}>"
class Line(BaseModel):
text = CharField()
search_text = TSVectorField()
person = ForeignKeyField(Person, backref="lines", null=True)
person = ForeignKeyField(Person, backref="lines", null=True, on_delete="CASCADE")
isnote = BooleanField(default=False)
ismeta = BooleanField(default=False)
starttime = IntegerField()
endtime = IntegerField()
episode = ForeignKeyField(Episode, backref="lines")
episode = ForeignKeyField(Episode, backref="lines", on_delete="CASCADE")
order = IntegerField()
class Meta:
indexes = ((("episode", "order"), True),)
def __str__(self):
return f"<Line: {self.pk}>"
class Phrase(BaseModel):
text = CharField()
count = IntegerField()
episode = ForeignKeyField(Episode, backref="phrase")
episode = ForeignKeyField(Episode, backref="phrase", on_delete="CASCADE")
class Meta:
indexes = ((("text", "episode"), True),)
def __str__(self):
return f"<Line: {self.text} ({self.pk})>"

View file

@ -74,7 +74,7 @@ for episode in Episode.select().where((Episode.phrases_imported == False) & (Epi
num_per_chunk = 100
chunks = chunked(phrases, num_per_chunk)
with alive_bar(len(phrases) // num_per_chunk + 1) as bar:
with alive_bar(len(phrases) // num_per_chunk + 1, title="saving") as bar:
for chunk in chunks:
bar()
Phrase.bulk_create(chunk)

View file

@ -21,15 +21,15 @@ def add_cors(response: Response) -> Response:
return response
def suggest(query: str, until: int, series: int, limit: int = 10) -> ModelSelect:
return Phrase.select(Phrase.text, Alias(fn.SUM(Phrase.count), "total_count")).join(Episode).where(
(Episode.series == series) &
def suggest(query: str, until: int, series: str, limit: int = 10) -> ModelSelect:
return Phrase.select(Phrase.text, Alias(fn.SUM(Phrase.count), "total_count")).join(Episode).join(Series).where(
(Episode.series.slug == series) &
(Episode.episode_number <= until) &
(Phrase.text.contains(query))
).group_by(Phrase.text).order_by(SQL("total_count DESC")).limit(limit)
def search(query: str, until: int, series: int, limit: int = 50) -> ModelSelect:
def search(query: str, until: int, series: str, limit: int = 50) -> ModelSelect:
a = Alias(fn.ts_rank_cd(Line.search_text, fn.websearch_to_tsquery('english', query), 1 + 4), "rank")
return Line.select(Line, Person, Episode, Series, a).where(
@ -37,7 +37,7 @@ def search(query: str, until: int, series: int, limit: int = 50) -> ModelSelect:
&
(Episode.episode_number <= until)
&
(Episode.series == series)
(Episode.series.slug == series)
).order_by(SQL("rank DESC")) \
.join(Person).switch(Line) \
.join(Episode).join(Series) \
@ -127,7 +127,13 @@ def series():
series_list = []
for series in Series.select():
series_list.append({"title": series.title, "id": series.id})
last_episode: Episode = Episode.select().where(Episode.series == series).order_by(
Episode.upload_date.desc()).limit(
1).get()
series_data = model_to_dict(series)
series_data["last_upload"] = last_episode.upload_date.strftime("%Y-%m-%d")
series_data["length"] = Episode.select().where(Episode.series == series).count()
series_list.append(series_data)
return jsonify({
"series": series_list
})
@ -144,6 +150,8 @@ def api_episodes():
series_data = []
for episode in episodes:
entry = model_to_dict(episode, exclude=[Episode.series, Episode.title])
if entry["upload_date"]:
entry["upload_date"] = entry["upload_date"].strftime("%Y-%m-%d")
series_data.append(entry)
data.append({
"meta": model_to_dict(series),

BIN
static/BarRoomBlitz.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
static/CallOfCthulhu.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
static/Cinderbrush.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
static/CriticalTrolls.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
static/CritterHug.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
static/DalensCloset.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
static/DiabloOneShot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
static/GrogsOneShot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
static/HoneyHeist.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
static/LiamsQuest.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
static/MiniPrimetime.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
static/SamsOneShot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
static/ShadowofWar.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
static/TheReturnofLiam.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/TheSearchForBob.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/ThursdayByNight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
static/ToThePoop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/UnDeadwood.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/campaign1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
static/campaign2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View file

@ -1,14 +1,17 @@
-- number of subtitle lines per person
select name, count(name) as count, sum(length(text)) as chars
from line
join person p on line.person_id = p.id
group by name
order by chars desc;
-- most common noun chunks
select text, sum(count) as count
from phrase
group by text
order by count desc;
-- longest noun chunks
select text, char_length(phrase.text) as len
from phrase
order by len desc;
@ -72,4 +75,4 @@ FROM "line" AS "t1"
WHERE ((("t1"."search_text" @@ websearch_to_tsquery('english', 'house')) AND ("t3"."episode_number" <= 1000)) AND
("t3"."season" = 1))
ORDER BY rank DESC
LIMIT 20
LIMIT 20;

12
typo.py
View file

@ -7,7 +7,7 @@ typos = {
"Sam": {"San", "Nott", "Sma", "Sasm", "Sm", "Ssam"},
"Travis": {"Tarvis", "Travs", "Travia", "Traivs", "Tavis", "Trvis"},
"Taliesin": {"Taiesin", "Talisin", "Talisen", "Taleisn", "Talisein", "Talieisin", "Talesin", "Talisan", "Taleisin",
"Talieisn", "Talisien", "Tailesin", "Tlaiesin"},
"Talieisn", "Talisien", "Tailesin", "Tlaiesin", "Tlaiesin"},
"Marisha": {"Beau", "Mariasha", "Maisha", "Marisa", "Marish", "Marihsa", "Marsha", "Marsisha", "Marishaa",
"Marihsha", "\\Marisha", "Marisah", "Marissa", "Marirsha", "Marisaha", "Mairsha", "Marshia", "Marsiha",
"Marishia", "Marsiah", "Matisha", "Mraisha", "Amrisha", "<Arisha"},
@ -16,10 +16,16 @@ typos = {
"Ashley": {"Ashly", "Ashely", "Ashey", "Aslhey", "Ahsley"},
"All": {"Everyone", "Everybody"},
"Mark": {"Marik"},
"Brian": {"Brain"},
"Brian": {"Brain", "\"Brian"},
"Joe": {"Jroe"},
"Man Off-Camera": {"Man Off Camera"},
"Off-Screen": {"Offscreen"}
"Off-Screen": {"Offscreen"},
"Anjali": {"Anajli"},
"H. Michael": {"H Michael", "H.Michael"},
"Allura": {"Alura"},
"Krystina": {"Krystin"},
"Michelle":{"Michlle"},
"Alicia":{"Alica"}
}
replacements = {}
for correct, typoset in typos.items():

View file

@ -17,7 +17,10 @@ def milliseconds_to_td(ms: int) -> timedelta:
def episode_speaker(series_title: str, episode: int) -> Optional[str]:
series = single_speaker[series_title]
try:
series = single_speaker[series_title]
except KeyError:
return "?"
if episode in series:
return series[episode]
return None
@ -41,6 +44,8 @@ def title_to_episodenumber(title: str, video_number: int) -> int:
except ValueError:
if title == "Campaign 1": # one-shots at the end of campaign 1
return video_number - 3
elif "Exandria" in title:
return 1
else:
raise

29178
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,8 @@
transform: translate(-50%, -50%);
//border: 2px solid $border-color;
z-index: 100;
max-height: 95vh;
overflow: auto;
@include media-breakpoint-down(md) {
width: 80%;
@ -82,3 +84,33 @@
{
opacity: 0;
}
.seriesSelector {
width: 95%;
.seriesList {
display: flex;
flex-wrap: wrap;
.series {
width: 50%;
cursor: pointer;
}
}
img {
width: 100%;
}
.flip-list-move {
transition: transform 1s;
}
.flip-list-enter, .flip-list-leave-to
/* .list-complete-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
.flip-list-leave-active {
position: absolute;
}
}

View file

@ -0,0 +1,61 @@
<template>
<div class="seriesSelector popup">
<h1>Select Series</h1>
<input type="checkbox" id="onlyCampaigns" v-model="onlyCampaigns">
<label for="onlyCampaigns">show only campaigns</label>
<input type="checkbox" id="showOneShots" v-model="showOneShots">
<label for="showOneShots">show One-Shots</label>
<input type="search" id="search" v-model="search">
<label for="search">search</label>
<transition-group name="flip-list" class="seriesList" tag="div">
<div class="series" v-for="series in sortedSeries" @click="selectSeries(series)" :key="series.id">
<span>{{ series.title }}</span>
<img :src="'http://127.0.0.1:5000/static/'+series.slug+'.webp'">
</div>
</transition-group>
</div>
</template>
<script lang="ts">
import Vue, {PropType} from "vue";
import {Series, SeriesData, ServerData} from "@/interfaces";
export default Vue.extend({
name: "SeriesSelector",
props: {
serverData: Object as PropType<ServerData>
},
data() {
return {
onlyCampaigns: false,
showOneShots: true,
search: ""
};
},
computed: {
sortedSeries(): SeriesData[] {
console.log(this.search);
return this.serverData.series.filter((series) => {
console.log(series.title.includes(this.search))
if (!series.is_campaign && this.onlyCampaigns) {
return false;
}
if (series.length === 1 && !this.showOneShots) {
return false;
}
return this.search === "" || series.title.toLowerCase().includes(this.search.toLowerCase());
}).sort((a, b) => {
return b.last_upload.localeCompare(a.last_upload);
});
}
},
methods: {
selectSeries(series: SeriesData): void {
// @ts-ignore
this.$parent.selectSeries(series);
}
}
});
</script>

View file

@ -9,6 +9,12 @@ export interface Series {
"is_campaign": boolean;
"single_speaker": boolean;
"title": string;
"slug": string;
}
export interface SeriesData extends Series {
"last_upload": string;
"length": number;
}
export interface Episode {
@ -44,19 +50,15 @@ export interface ServerMessage {
message: string;
}
export interface SeriesNames {
"id": number;
"title": string;
}
export interface ServerData {
"series": SeriesNames[];
"series": SeriesData[];
}
export interface EpisodeDetailed extends Episode {
"downloaded": boolean;
"text_imported": boolean;
"phrases_imported": boolean;
"upload_date": string;
}
export interface SeriesData {

View file

@ -11,7 +11,7 @@ export default new Router({
routes: [
{
path: "/",
redirect: "/search/2/10/",
redirect: "/search/campaign2/10/",
},
{
path: "/episodes",
@ -20,11 +20,11 @@ export default new Router({
},
{
path: "/:something/",
redirect: "/search/2/10/",
redirect: "/search/campaign2/10/",
},
{
path: "/:something/:something/",
redirect: "/search/2/10/",
path: "/:something/:somethingElse/",
redirect: "/search/campaign2/10/",
},
{
path: "/search/:series/:episode/:keyword?",

View file

@ -14,6 +14,7 @@
<th>Title</th>
<th>Episode</th>
<th>Video</th>
<th>Upload Date</th>
<th>Subtitles available and fetched</th>
<th>Subtitles imported in search</th>
<th>Phrases imported for search suggestions</th>
@ -27,6 +28,7 @@
</td>
<td>{{ episode.episode_number }}</td>
<td>{{ episode.video_number }}</td>
<td>{{episode.upload_date}}</td>
<td class="text-center">
<CheckMark :status="episode.downloaded"></CheckMark>
</td>

View file

@ -6,8 +6,9 @@
<span>Find your favourite Critical Role quote!</span>
</div>
<transition name="fade">
<div id="page-mask" v-if="showIntro || showYtOptIn"></div>
<div id="page-mask" v-if="showIntro || showYtOptIn|| showSeriesSelector"></div>
</transition>
<SeriesSelector v-if="showSeriesSelector" :serverData="serverData"></SeriesSelector>
<div v-if="showIntro" class="showIntro popup">
<div class="title"><h1>Critical Role Search</h1></div>
<div>
@ -76,11 +77,15 @@
class="form-control" type="number" v-model="episode"
min="1" max="300">
<span>in</span>
<select title="campaign selection" class="custom-select" v-model="series">
<option v-for="series in serverData.series" v-bind:value="series.id">
{{ series.title }}
</option>
</select>
<!-- <select title="campaign selection" class="custom-select" v-model="series">-->
<!-- <option v-for="series in serverData.series" v-bind:value="series.slug">-->
<!-- {{ series.title }}-->
<!-- </option>-->
<!-- </select>-->
<button class="btn btn-outline-primary" @click="showSeriesSelector=true">
{{ seriesTitle }}
</button>
</div>
<b-alert v-if="error" show :variant="error.status">{{ error.message }}</b-alert>
<div class="entry" v-for="result in searchResult">
@ -120,13 +125,14 @@ import Vue from "vue";
// @ts-ignore
import Autocomplete from "@trevoreyre/autocomplete-vue";
// import "@trevoreyre/autocomplete-vue/dist/style.css";
import {Line, Result, ServerData, ServerMessage} from "@/interfaces";
import {Line, Result, Series, ServerData, ServerMessage} from "@/interfaces";
import {BAlert, BIcon, BIconPlayFill} from "bootstrap-vue";
// @ts-ignore
import VueYoutube from "vue-youtube";
import debounce from "lodash-es/debounce";
import {baseURL} from "@/utils";
import SeriesSelector from "@/components/SeriesSelector.vue";
Vue.use(VueYoutube);
@ -134,6 +140,7 @@ Vue.use(VueYoutube);
export default Vue.extend({
name: "home",
components: {
SeriesSelector,
Autocomplete,
BAlert,
BIcon,
@ -141,7 +148,7 @@ export default Vue.extend({
},
data() {
return {
serverData: {"series": []} as ServerData,
serverData: {series: []} as ServerData,
searchResult: [] as Result[],
keyword: this.$route.params.keyword,
series: this.$route.params.series,
@ -153,7 +160,8 @@ export default Vue.extend({
showYT: false,
ytResult: undefined as Result | undefined,
ytWidth: 640,
showIntro: true
showIntro: true,
showSeriesSelector: false
};
},
mounted(): void {
@ -164,7 +172,7 @@ export default Vue.extend({
this.ytOptIn = localStorage.ytOptIn;
}
if (this.series == null) {
this.series = "2";
this.series = "campaign2";
}
if (this.episode == null) {
this.episode = "10";
@ -302,6 +310,10 @@ export default Vue.extend({
this.ytVideoID = undefined;
this.ytResult = undefined;
},
selectSeries(series: Series): void {
this.series = series.slug;
this.showSeriesSelector = false;
}
},
computed: {
ytLink(): string {
@ -314,6 +326,22 @@ export default Vue.extend({
const min = Math.floor(starttime / 60);
const sec = Math.floor(starttime % 60);
return `https://www.youtube.com/watch?v=${id}&t=${min}m${sec}s`;
},
seriesFromSlug(): Series | undefined {
if (!this.series) {
return undefined;
}
return this.serverData.series.find((series) => {
return series.slug === this.series;
});
},
seriesTitle(): string {
const series = this.seriesFromSlug;
if (series) {
return series.title;
} else {
return this.series;
}
}
},
watch: {

File diff suppressed because it is too large Load diff