Initial commit — YT Hub

Self-hosted personal YouTube management app.
FastAPI + SQLite backend, React + Vite + Tailwind frontend.
Dockerfiles and compose included for Portainer deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
inputnoise
2026-05-25 20:09:04 +02:00
commit 1827dd6c4e
63 changed files with 14480 additions and 0 deletions

135
frontend/src/api/index.js Normal file
View File

@@ -0,0 +1,135 @@
import axios from "axios";
const api = axios.create({ baseURL: "/api" });
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(err);
}
);
export default api;
// Auth
export const login = (username, password) => {
const form = new FormData();
form.append("username", username);
form.append("password", password);
return api.post("/auth/login", form);
};
export const register = (data) => api.post("/auth/register", data);
export const getMe = () => api.get("/auth/me");
// Search
export const search = (q, live = false) =>
api.get("/search", { params: { q, live } });
export const getSearchHistory = () => api.get("/search/history");
// Channels
export const getChannels = () => api.get("/channels");
export const getChannelFeed = (offset = 0) => api.get("/channels/feed", { params: { offset, limit: 24 } });
export const getChannel = (id) => api.get(`/channels/${id}`);
export const syncAllChannels = () => api.post("/channels/sync-all");
export const getChannelVideos = (id) => api.get(`/channels/${id}/videos`);
export const followChannel = (id) => api.post(`/channels/${id}/follow`);
export const unfollowChannel = (id) => api.delete(`/channels/${id}/follow`);
export const indexChannel = (id) => api.post(`/channels/${id}/index`);
export const followChannelByUrl = (data) => api.post("/channels/follow-by-url", data);
export const setChannelAutoDownload = (id, value) => api.patch(`/channels/${id}/auto-download`, { auto_download: value });
export const markChannelsSeen = () => api.post("/channels/mark-seen");
export const followBulk = (handles) => api.post("/channels/follow-bulk", { handles });
export const updateChannelNotes = (id, notes) => api.patch(`/channels/${id}/notes`, { notes });
export const muteChannel = (id) => api.post(`/channels/${id}/mute`);
export const unmuteChannel = (id) => api.delete(`/channels/${id}/mute`);
export const getChannelGroups = () => api.get("/channels/groups");
export const createChannelGroup = (name) => api.post("/channels/groups", { name });
export const deleteChannelGroup = (id) => api.delete(`/channels/groups/${id}`);
export const renameChannelGroup = (id, name) => api.patch(`/channels/groups/${id}`, { name });
export const addChannelToGroup = (groupId, channelId) => api.post(`/channels/groups/${groupId}/channels/${channelId}`);
export const removeChannelFromGroup = (groupId, channelId) => api.delete(`/channels/groups/${groupId}/channels/${channelId}`);
export const bulkChannelAction = (channel_ids, action) => api.post("/channels/bulk-action", { channel_ids, action });
// Videos
export const homeFeed = (page = 0, limit = 25, mode = "ranked", duration = "") =>
api.get("/videos/home-feed", { params: { offset: page * limit, limit, mode, ...(duration ? { duration } : {}) } });
export const continueWatching = () => api.get("/videos/continue-watching");
export const longVideos = () => api.get("/videos/long");
export const surpriseMe = () => api.get("/videos/surprise");
export const getVideo = (id) => api.get(`/videos/${id}`);
export const getVideoByYtId = (ytId) => api.get(`/videos/by-yt/${ytId}`);
export const updateProgress = (id, data) => api.patch(`/videos/${id}/progress`, data);
export const toggleQueue = (id) => api.post(`/videos/${id}/queue`);
export const getQueue = () => api.get("/videos/queue");
export const toggleLike = (id) => api.post(`/videos/${id}/like`);
export const getLikedVideos = () => api.get("/videos/liked");
export const rateVideo = (id, rating) => api.post(`/videos/${id}/rate`, { rating });
export const getRelatedVideos = (videoId, mode = "weighted") => api.get(`/videos/${videoId}/related`, { params: { mode } });
export const getHistory = (page = 0, limit = 25, channel_id = null) =>
api.get("/videos/history", { params: { offset: page * limit, limit, ...(channel_id ? { channel_id } : {}) } });
export const getBookmarks = (videoId) => api.get(`/videos/${videoId}/bookmarks`);
export const createBookmark = (videoId, data) => api.post(`/videos/${videoId}/bookmarks`, data);
export const updateBookmark = (videoId, bookmarkId, data) => api.patch(`/videos/${videoId}/bookmarks/${bookmarkId}`, data);
export const deleteBookmark = (videoId, bookmarkId) => api.delete(`/videos/${videoId}/bookmarks/${bookmarkId}`);
export const importChapters = (videoId) => api.post(`/videos/${videoId}/bookmarks/import-chapters`);
export const clearChapters = (videoId) => api.delete(`/videos/${videoId}/bookmarks/clear-chapters`);
// Downloads
export const createDownload = (youtube_video_id, quality) =>
api.post("/downloads", { youtube_video_id, ...(quality ? { quality } : {}) });
export const getDownloads = () => api.get("/downloads");
export const getDownload = (id) => api.get(`/downloads/${id}`);
export const deleteDownload = (id) => api.delete(`/downloads/${id}`);
export const deleteAllDownloads = () => api.delete("/downloads/all");
export const restoreDownload = (id) => api.post(`/downloads/${id}/restore`);
export const downloadChannel = (channelId) => api.post(`/downloads/channel/${channelId}`);
export const downloadFollowing = () => api.post("/downloads/following");
// Export
export const exportData = () => api.get("/export", { responseType: "blob" });
// Settings
export const getSettings = () => api.get("/settings");
export const updateSettings = (data) => api.patch("/settings", data);
// Discovery
export const getDiscovery = (offset = 0, limit = 50) =>
api.get("/discovery", { params: { offset, limit } });
export const dismissDiscoveryVideo = (youtubeVideoId) =>
api.post(`/discovery/videos/${youtubeVideoId}/dismiss`);
export const getDiscoveryVideos = (offset = 0, limit = 50) =>
api.get("/discovery/videos", { params: { offset, limit } });
export const followDiscovery = (channelId) =>
api.post(`/discovery/${channelId}/follow`);
export const dismissDiscovery = (channelId) =>
api.post(`/discovery/${channelId}/dismiss`);
export const refreshDiscovery = () => api.post("/discovery/refresh");
export const getCommunityShelf = () => api.get("/discovery/community");
// Stats
export const getStats = () => api.get("/stats");
// Admin
export const getAdminUsers = () => api.get("/admin/users");
export const deleteAdminUser = (id) => api.delete(`/admin/users/${id}`);
export const getAdminConfig = () => api.get("/admin/config");
export const updateAdminConfig = (data) => api.patch("/admin/config", data);
// Collections
export const getCollections = () => api.get("/collections");
export const createCollection = (name) => api.post("/collections", { name });
export const renameCollection = (id, name) => api.patch(`/collections/${id}`, { name });
export const deleteCollection = (id) => api.delete(`/collections/${id}`);
export const getCollectionVideos = (id) => api.get(`/collections/${id}/videos`);
export const addToCollection = (collectionId, videoId) => api.post(`/collections/${collectionId}/videos`, { video_id: videoId });
export const removeFromCollection = (collectionId, videoId) => api.delete(`/collections/${collectionId}/videos/${videoId}`);