Skip to content

Commit

Permalink
Merge branch 'rebane2001:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
CanOfSocks authored Jul 23, 2024
2 parents 0cc3839 + 122417e commit 42605b0
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 52 deletions.
19 changes: 16 additions & 3 deletions hobune/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ def is_full_channel(root):

def process_channel(channels, v, full):
if full:
channel_id = v.get("channel_id", "NA")
channel_id = v.get("channel_id", v.get("uploader_id", "NA"))
channel_name = v["uploader"]
uploader_id = v.get("uploader_id")
channel_username = uploader_id if uploader_id[0] != "@" and uploader_id != channel_id else None
channel_handle = uploader_id if uploader_id[0] == "@" else None
channel_username = uploader_id if uploader_id and uploader_id[0] != "@" and uploader_id != channel_id else None
channel_handle = uploader_id if uploader_id and uploader_id[0] == "@" else None
if not channel_id:
raise KeyError("channel_id not found")
if channel_id not in channels:
Expand Down Expand Up @@ -94,6 +94,19 @@ def initialize_channels(config):
channels[channel_id].videos.append(v)
except Exception as e:
print(f"Error processing {file}", e)
# Fix username-only entries with no channel ID
username_map = {}
for _, channel in channels.items():
if channel.username and channel.username != channel.id:
username_map[channel.username] = channel.id
for username, channel_id in username_map.items():
channel = channels.pop(username, None)
if channel:
channels[channel_id].removed_count += channel.removed_count
channels[channel_id].unlisted_count += channel.unlisted_count
channels[channel_id].videos += channel.videos
channels[channel_id].names = channels[channel_id].names | channel.names

return channels


Expand Down
2 changes: 1 addition & 1 deletion hobune/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def quote_url(url):
if os.path.sep == "\\":
url = url.replace("\\", "/")
url = url.replace("%5C", "/")
return urllib.parse.quote(url)
return urllib.parse.quote(url).replace("%3A", ":")


def extract_ids_from_txt(filename):
Expand Down
4 changes: 2 additions & 2 deletions hobune/videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ def create_video_pages(config, channels, templates, html_ext):
ytlink=f"<a class=\"ytlink\" href=https://www.youtube.com/watch?v={html.escape(v['id'])}>YT</a>",
description=html.escape(v['description']).replace('\n', '<br>'),
views=v['view_count'],
uploader_url=f"{config.web_root}channels/{html.escape(v['channel_id'])}{html_ext}" if is_full_channel(root) else f'{config.web_root}channels/other{html_ext}',
uploader_id={html.escape(v['channel_id'])},
uploader_url=f"{config.web_root}channels/{html.escape(v.get('channel_id', v.get('uploader_id')))}{html_ext}" if is_full_channel(root) else f'{config.web_root}channels/other{html_ext}',
uploader_id={html.escape(v.get('channel_id', v.get('uploader_id')))},
uploader=html.escape(v['uploader']),
date=f"{v['upload_date'][:4]}-{v['upload_date'][4:6]}-{v['upload_date'][6:]}",
video=quote_url(mp4path),
Expand Down
1 change: 1 addition & 0 deletions hobune_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Strikethrough means already implemented.
- Extract metadata gathering from info.json into a separate module so it can easily be extended for fallbacks
- Single video/channel mode
- Video length
- Add stats for how long creating the HTML took
121 changes: 75 additions & 46 deletions templates/hobune.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,83 @@
/** Sorts videos based on dropdown selection. */
function channelSort() {
const sortOption = document.querySelector(".sort").value;
const [sortBy, direction] = sortOption.split("-");
const isInt = sortBy !== "search";
const container = document.querySelector(".channels.flex-grid");
[...container.children]
.sort((a,b)=>{
const dir = direction ? 1 : -1;
let valA = a.dataset[sortBy];
let valB = b.dataset[sortBy];
if (isInt) {
valA = parseInt(valA);
valB = parseInt(valB);
}
return (valA>valB?1:-1)*dir;
})
.forEach(node=>container.appendChild(node));
const sortOption = document.querySelector(".sort").value;
const [sortBy, direction] = sortOption.split("-");
const isInt = sortBy !== "search";
const container = document.querySelector(".channels.flex-grid");
[...container.children]
.sort((a,b)=>{
const dir = direction ? 1 : -1;
let valA = a.dataset[sortBy];
let valB = b.dataset[sortBy];
if (isInt) {
valA = parseInt(valA);
valB = parseInt(valB);
}
return (valA>valB?1:-1)*dir;
})
.forEach(node=>container.appendChild(node));
}

/** Returns a RegExp if searchTerm matches the /expression/flags format. */
function getSearchRegex(searchTerm) {
const regexParts = searchTerm.match(/^\/(.*)\/([dgimsuvy]*)$/);
try {
return regexParts && new RegExp(regexParts[1], regexParts[2]);
} catch(err) { console.error(err); }
}

/**
* Filters visible videos to only those that match the search query.
* Non-public videos can be filtered by adding unlisted/removed (to show)
* or !unlisted/!removed (to hide) to the search query.
* RegExp can be used by writing the query in the /exp/flags format.
* Search is case-insensitive except for RegExp which requires the i flag.
*/
function channelSearch() {
let searchTerm = document.querySelector(".search").value.toLowerCase();
const allowedClasses = [];
const filteredClasses = [];
const availableClasses = ["unlisted", "removed"];
for (const availableClass of availableClasses) {
if (searchTerm.includes("!" + availableClass)) {
filteredClasses.push(availableClass);
searchTerm = searchTerm.replace("!" + availableClass, "");
} else if (searchTerm.includes(availableClass)) {
allowedClasses.push(availableClass);
searchTerm = searchTerm.replace(availableClass, "");
}
}

document.querySelectorAll('.searchable').forEach((e) => {
let filtered = false;
for (const c of allowedClasses) {
if (!e.querySelector(`.${c}`)) filtered = true;
}
for (const c of filteredClasses) {
if (e.querySelector(`.${c}`)) filtered = true;
}

if (!filtered && (searchTerm === "" || e.dataset.search.toLowerCase().includes(searchTerm))) {
e.classList.remove("hide");
} else {
e.classList.add("hide");
}
});
const searchBox = document.querySelector(".search");
if (!searchBox) return;

let searchTerm = searchBox.value;

const allowedClasses = [];
const filteredClasses = [];
const availableClasses = ["unlisted", "removed"];
for (const availableClass of availableClasses) {
if (searchTerm.includes("!" + availableClass)) {
filteredClasses.push(availableClass);
searchTerm = searchTerm.replace("!" + availableClass, "");
} else if (searchTerm.includes(availableClass)) {
allowedClasses.push(availableClass);
searchTerm = searchTerm.replace(availableClass, "");
}
}

const searchRegex = getSearchRegex(searchTerm);
searchTerm = searchTerm.toLowerCase();

document.querySelectorAll('.searchable').forEach((e) => {
let filtered = false;
for (const c of allowedClasses) {
if (!e.querySelector(`.${c}`)) filtered = true;
}
for (const c of filteredClasses) {
if (e.querySelector(`.${c}`)) filtered = true;
}

if (!filtered && (searchTerm === "" ||
(!searchRegex && e.dataset.search.toLowerCase().includes(searchTerm)) ||
(searchRegex && searchRegex.test(e.dataset.search))
)) {
e.classList.remove("hide");
} else {
e.classList.add("hide");
}
});
}

window.addEventListener("load", () => {
channelSearch();
// Always perform the search on page load, because:
// 1) Navigating to a previous page will retain the textbox contents
// 2) The search may have been performed while the DOM wasn't fully loaded
channelSearch();
});

0 comments on commit 42605b0

Please sign in to comment.