Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
e38edc45fa | |||
93bde35cbf | |||
ecc4f47321 | |||
a7aca14def | |||
f0b4a38775 | |||
54bf779a55 |
13 changed files with 251 additions and 35 deletions
|
@ -41,3 +41,5 @@ tasks:
|
||||||
deps: [frontend:generate]
|
deps: [frontend:generate]
|
||||||
cmds:
|
cmds:
|
||||||
- pnpm run build
|
- pnpm run build
|
||||||
|
- rm -rf ../dist
|
||||||
|
- mv dist ../dist
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<title>Vite + Solid + TS</title>
|
<title>Podcast Server</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import type { Options } from '@hey-api/client-fetch';
|
import type { Options } from '@hey-api/client-fetch';
|
||||||
import { queryOptions, type MutationOptions } from '@tanstack/solid-query';
|
import { queryOptions, type MutationOptions } from '@tanstack/solid-query';
|
||||||
import type { GetAppConfigData, ReadUserData, ReadPodcastsData, CreatePodcastData, CreatePodcastError, CreatePodcastResponse, DeletePodcastData, DeletePodcastError, ReadPodcastData, UpdatePodcastData, UpdatePodcastError, UpdatePodcastResponse, UpdatePodcastImageData, UpdatePodcastImageError, UpdatePodcastImageResponse, ReadEpisodesData, AdminUploadEpisodeData, AdminUploadEpisodeError, DeleteEpisodeData, DeleteEpisodeError, ReadEpisodeData, UpdateEpisodeData, UpdateEpisodeError, UpdateEpisodeResponse, EpisodeAdditionalUploadData, EpisodeAdditionalUploadError, EpisodeAdditionalUploadResponse, GetPodcastFeedData, GetEpisodeOrCoverData } from '../types.gen';
|
import type { GetAppConfigData, ReadUserData, ReadPodcastsData, CreatePodcastData, CreatePodcastError, CreatePodcastResponse, DeletePodcastData, DeletePodcastError, ReadPodcastData, UpdatePodcastData, UpdatePodcastError, UpdatePodcastResponse, UpdatePodcastImageData, UpdatePodcastImageError, UpdatePodcastImageResponse, ReadEpisodesData, AdminUploadEpisodeData, AdminUploadEpisodeError, DeleteEpisodeData, DeleteEpisodeError, ReadEpisodeData, UpdateEpisodeData, UpdateEpisodeError, UpdateEpisodeResponse, EpisodeAdditionalUploadData, EpisodeAdditionalUploadError, EpisodeAdditionalUploadResponse, GetPodcastFeedData, GetEpisodeOrCoverData, ServeAppData } from '../types.gen';
|
||||||
import { getAppConfig, readUser, readPodcasts, createPodcast, deletePodcast, readPodcast, updatePodcast, updatePodcastImage, readEpisodes, adminUploadEpisode, deleteEpisode, readEpisode, updateEpisode, episodeAdditionalUpload, getPodcastFeed, getEpisodeOrCover, client } from '../sdk.gen';
|
import { getAppConfig, readUser, readPodcasts, createPodcast, deletePodcast, readPodcast, updatePodcast, updatePodcastImage, readEpisodes, adminUploadEpisode, deleteEpisode, readEpisode, updateEpisode, episodeAdditionalUpload, getPodcastFeed, getEpisodeOrCover, serveApp, client } from '../sdk.gen';
|
||||||
|
|
||||||
type QueryKey<TOptions extends Options> = [
|
type QueryKey<TOptions extends Options> = [
|
||||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||||
|
@ -371,3 +371,22 @@ export const getEpisodeOrCoverOptions = (options: Options<GetEpisodeOrCoverData>
|
||||||
queryKey: getEpisodeOrCoverQueryKey(options)
|
queryKey: getEpisodeOrCoverQueryKey(options)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const serveAppQueryKey = (options: Options<ServeAppData>) => [
|
||||||
|
createQueryKey('serveApp', options)
|
||||||
|
];
|
||||||
|
|
||||||
|
export const serveAppOptions = (options: Options<ServeAppData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await serveApp({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: serveAppQueryKey(options)
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { createClient, createConfig, type Options, formDataBodySerializer } from '@hey-api/client-fetch';
|
import { createClient, createConfig, type Options, formDataBodySerializer } from '@hey-api/client-fetch';
|
||||||
import type { GetAppConfigData, GetAppConfigResponse, ReadUserData, ReadUserResponse, ReadPodcastsData, ReadPodcastsResponse, CreatePodcastData, CreatePodcastResponse, CreatePodcastError, DeletePodcastData, DeletePodcastError, ReadPodcastData, ReadPodcastResponse, ReadPodcastError, UpdatePodcastData, UpdatePodcastResponse, UpdatePodcastError, UpdatePodcastImageData, UpdatePodcastImageResponse, UpdatePodcastImageError, ReadEpisodesData, ReadEpisodesResponse, ReadEpisodesError, AdminUploadEpisodeData, AdminUploadEpisodeError, DeleteEpisodeData, DeleteEpisodeError, ReadEpisodeData, ReadEpisodeResponse, ReadEpisodeError, UpdateEpisodeData, UpdateEpisodeResponse, UpdateEpisodeError, EpisodeAdditionalUploadData, EpisodeAdditionalUploadResponse, EpisodeAdditionalUploadError, GetPodcastFeedData, GetPodcastFeedError, GetEpisodeOrCoverData, GetEpisodeOrCoverError } from './types.gen';
|
import type { GetAppConfigData, GetAppConfigResponse, ReadUserData, ReadUserResponse, ReadPodcastsData, ReadPodcastsResponse, CreatePodcastData, CreatePodcastResponse, CreatePodcastError, DeletePodcastData, DeletePodcastError, ReadPodcastData, ReadPodcastResponse, ReadPodcastError, UpdatePodcastData, UpdatePodcastResponse, UpdatePodcastError, UpdatePodcastImageData, UpdatePodcastImageResponse, UpdatePodcastImageError, ReadEpisodesData, ReadEpisodesResponse, ReadEpisodesError, AdminUploadEpisodeData, AdminUploadEpisodeError, DeleteEpisodeData, DeleteEpisodeError, ReadEpisodeData, ReadEpisodeResponse, ReadEpisodeError, UpdateEpisodeData, UpdateEpisodeResponse, UpdateEpisodeError, EpisodeAdditionalUploadData, EpisodeAdditionalUploadResponse, EpisodeAdditionalUploadError, GetPodcastFeedData, GetPodcastFeedError, GetEpisodeOrCoverData, GetEpisodeOrCoverError, ServeAppData, ServeAppError } from './types.gen';
|
||||||
|
|
||||||
export const client = createClient(createConfig());
|
export const client = createClient(createConfig());
|
||||||
|
|
||||||
|
@ -187,7 +187,17 @@ export const getPodcastFeed = <ThrowOnError extends boolean = false>(options: Op
|
||||||
*/
|
*/
|
||||||
export const getEpisodeOrCover = <ThrowOnError extends boolean = false>(options: Options<GetEpisodeOrCoverData, ThrowOnError>) => {
|
export const getEpisodeOrCover = <ThrowOnError extends boolean = false>(options: Options<GetEpisodeOrCoverData, ThrowOnError>) => {
|
||||||
return (options?.client ?? client).get<unknown, GetEpisodeOrCoverError, ThrowOnError>({
|
return (options?.client ?? client).get<unknown, GetEpisodeOrCoverError, ThrowOnError>({
|
||||||
url: '/{podcast_id}/{filename}',
|
url: '/files/{podcast_id}/{filename}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve App
|
||||||
|
*/
|
||||||
|
export const serveApp = <ThrowOnError extends boolean = false>(options: Options<ServeAppData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<unknown, ServeAppError, ThrowOnError>({
|
||||||
|
url: '/{full_path}',
|
||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -444,7 +444,7 @@ export type GetEpisodeOrCoverData = {
|
||||||
filename: string;
|
filename: string;
|
||||||
};
|
};
|
||||||
query?: never;
|
query?: never;
|
||||||
url: '/{podcast_id}/{filename}';
|
url: '/files/{podcast_id}/{filename}';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetEpisodeOrCoverErrors = {
|
export type GetEpisodeOrCoverErrors = {
|
||||||
|
@ -462,3 +462,28 @@ export type GetEpisodeOrCoverResponses = {
|
||||||
*/
|
*/
|
||||||
200: unknown;
|
200: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServeAppData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
full_path: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/{full_path}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServeAppErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServeAppError = ServeAppErrors[keyof ServeAppErrors];
|
||||||
|
|
||||||
|
export type ServeAppResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: unknown;
|
||||||
|
};
|
|
@ -1 +1 @@
|
||||||
export const SERVER_URL = import.meta.env.PROD ? "" : "http://localhost:8000";
|
export const SERVER_URL = import.meta.env.PROD ? window.location.origin : "http://localhost:8000";
|
||||||
|
|
|
@ -83,6 +83,10 @@ const routes = [
|
||||||
component: () => (
|
component: () => (
|
||||||
<Protected>{lazy(() => import("./routes/admin/episode"))()}</Protected>
|
<Protected>{lazy(() => import("./routes/admin/episode"))()}</Protected>
|
||||||
),
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:podcastId",
|
||||||
|
component: lazy(() => import("./routes/podcast")),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ export default function AdminPodcast() {
|
||||||
<Show when={podcastQuery.data?.image_filename}>
|
<Show when={podcastQuery.data?.image_filename}>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<figure class="image is-128x128">
|
<figure class="image is-128x128">
|
||||||
<img src={new URL(params.podcastId + "/" + podcastQuery.data!.image_filename, SERVER_URL).href} />
|
<img src={new URL(`/files/${params.podcastId}/${podcastQuery.data!.image_filename}`, SERVER_URL).href} />
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default function AdminPodcasts() {
|
||||||
<Show when={podcast.image_filename}>
|
<Show when={podcast.image_filename}>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<figure class="image is-64x64">
|
<figure class="image is-64x64">
|
||||||
<img src={new URL(podcast.id + "/" + podcast.image_filename, SERVER_URL).href} />
|
<img src={new URL("/files/" + podcast.id + "/" + podcast.image_filename, SERVER_URL).href} />
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
85
client/src/routes/podcast.tsx
Normal file
85
client/src/routes/podcast.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
import { readPodcastOptions, readEpisodesOptions } from "../client/@tanstack/solid-query.gen";
|
||||||
|
import { ErrorBoundary, For, Show, Suspense } from "solid-js";
|
||||||
|
import Error from "../components/error";
|
||||||
|
import Loading from "../components/loading";
|
||||||
|
import { DownloadIcon, MusicIcon } from "lucide-solid";
|
||||||
|
import { SERVER_URL } from "../constants";
|
||||||
|
|
||||||
|
export default function Podcast() {
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const podcastQuery = createQuery(() => ({
|
||||||
|
...readPodcastOptions({
|
||||||
|
path: {
|
||||||
|
podcast_id: params.podcastId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
const episodeQuery = createQuery(() => ({
|
||||||
|
...readEpisodesOptions({
|
||||||
|
path: {
|
||||||
|
podcast_id: params.podcastId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<ErrorBoundary fallback={(err, reset) => <Error message={err} reset={reset} />}>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<section class="hero is-medium is-primary">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="columns">
|
||||||
|
<Show when={podcastQuery.data?.image_filename}>
|
||||||
|
<div class="column">
|
||||||
|
<figure class="image is-128x128">
|
||||||
|
<img src={new URL(`/files/${params.podcastId}/${podcastQuery.data!.image_filename}`, SERVER_URL).href} />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="column is-full">
|
||||||
|
<h1 class="title">{podcastQuery.data?.name}</h1>
|
||||||
|
<p class="subtitle">{podcastQuery.data?.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container is-max-desktop">
|
||||||
|
<For each={episodeQuery.data}>
|
||||||
|
{(episode) => (
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
<p class="image is-64x64 has-background-primary is-flex is-align-content-center">
|
||||||
|
<MusicIcon class="p-4 has-text-dark" />
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<p class="title is-5">{episode.name}</p>
|
||||||
|
<Show when={episode.publish_date}>
|
||||||
|
<p class="subtitle is-6">{new Date(episode.publish_date!).toLocaleDateString()}</p>
|
||||||
|
</Show>
|
||||||
|
<figure class="m-0 mb-4">
|
||||||
|
<audio controls src={new URL(`/files/${episode.podcast_id}/${episode.id}.m4a`, SERVER_URL).toString()}></audio>
|
||||||
|
</figure>
|
||||||
|
<Show when={episode.description_html}>
|
||||||
|
<p innerHTML={episode.description_html!}></p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-right">
|
||||||
|
<a href={new URL(`/files/${episode.podcast_id}/${episode.id}.m4a`, SERVER_URL).toString()}><DownloadIcon /></a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
|
@ -23,9 +23,8 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["src/types/*"]
|
"*": ["./src/types/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "src/types"]
|
"include": ["src", "src/types"]
|
||||||
|
|
|
@ -1,11 +1,44 @@
|
||||||
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Callable, List, Optional, Tuple
|
from typing import Callable, Final, List, Optional, Tuple, TypedDict
|
||||||
|
|
||||||
from fastapi import HTTPException, UploadFile
|
from fastapi import HTTPException, UploadFile
|
||||||
|
|
||||||
from models import PodcastEpisode
|
from models import PodcastEpisode
|
||||||
|
|
||||||
|
TRACK_LIST_HEADING: Final[str] = "**Track list**"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackListItem(TypedDict):
|
||||||
|
title: Optional[str]
|
||||||
|
artist: Optional[str]
|
||||||
|
timestamp: Optional[timedelta]
|
||||||
|
|
||||||
|
|
||||||
|
def update_episode_tracklist(
|
||||||
|
episode: PodcastEpisode, track_list: List[TrackListItem]
|
||||||
|
) -> Optional[PodcastEpisode]:
|
||||||
|
if len(track_list) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
description = (
|
||||||
|
episode.description.split(TRACK_LIST_HEADING)[0].strip()
|
||||||
|
if episode.description is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
description += f"\n\n{TRACK_LIST_HEADING}\n\n"
|
||||||
|
|
||||||
|
sorted_tracks = sorted(track_list, key=lambda x: x["timestamp"].total_seconds())
|
||||||
|
|
||||||
|
for i, track in enumerate(sorted_tracks):
|
||||||
|
description += f"{i + 1}. {track.get('title', 'ID')} _- {track.get('artist', 'ID')} [{str(track.get('timestamp', timedelta(seconds=0)))}]_\n"
|
||||||
|
|
||||||
|
episode.description = description.strip()
|
||||||
|
|
||||||
|
return episode
|
||||||
|
|
||||||
|
|
||||||
async def djuced_track_list(
|
async def djuced_track_list(
|
||||||
episode: PodcastEpisode, file: UploadFile
|
episode: PodcastEpisode, file: UploadFile
|
||||||
|
@ -16,7 +49,7 @@ async def djuced_track_list(
|
||||||
if root.tag != "recordEvents":
|
if root.tag != "recordEvents":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tracks = []
|
tracks: List[TrackListItem] = []
|
||||||
|
|
||||||
for track in root.iter("track"):
|
for track in root.iter("track"):
|
||||||
title = track.get("song")
|
title = track.get("song")
|
||||||
|
@ -29,21 +62,54 @@ async def djuced_track_list(
|
||||||
if len(title_segments) == 2:
|
if len(title_segments) == 2:
|
||||||
artist, title = title_segments
|
artist, title = title_segments
|
||||||
|
|
||||||
for interval in intervals:
|
if len(intervals) > 0:
|
||||||
tracks.append((float(interval.get("start")), title, artist))
|
tracks.append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"artist": artist,
|
||||||
|
"timestamp": timedelta(seconds=float(intervals[0].get("start"))),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# sort by start time
|
return update_episode_tracklist(episode, tracks)
|
||||||
tracks = sorted(tracks, key=lambda x: x[0], reverse=False)
|
|
||||||
|
|
||||||
# update description
|
|
||||||
track_list_str = ""
|
|
||||||
|
|
||||||
for i, (t, title, artist) in enumerate(tracks):
|
async def rekordbox_track_list(
|
||||||
time = timedelta(seconds=round(t))
|
episode: PodcastEpisode, file: UploadFile
|
||||||
track_list_str += f"{i + 1}. {title} _- {artist} [{time}]_\n"
|
) -> Optional[PodcastEpisode]:
|
||||||
|
if not file.filename.endswith(".cue"):
|
||||||
|
return None
|
||||||
|
|
||||||
episode.description += "\n\n**Track list**\n\n" + track_list_str
|
tracks: List[TrackListItem] = []
|
||||||
return episode
|
current_track: TrackListItem = {}
|
||||||
|
|
||||||
|
content = (await file.read()).decode("utf-8")
|
||||||
|
|
||||||
|
for line in content.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("TITLE"):
|
||||||
|
title = re.search(r'"(.*?)"', line).group(1)
|
||||||
|
title = re.sub(
|
||||||
|
r"\s*\((Clean Extended|Clean|Extended)\)", "", title
|
||||||
|
) # Remove specific suffixes
|
||||||
|
current_track["title"] = title
|
||||||
|
elif line.startswith("PERFORMER"):
|
||||||
|
current_track["artist"] = re.search(r'"(.*?)"', line).group(1)
|
||||||
|
elif line.startswith("INDEX 01"):
|
||||||
|
time_match = re.search(r"INDEX 01 (\d{2}):(\d{2}):(\d{2})", line)
|
||||||
|
if time_match:
|
||||||
|
hours = int(time_match.group(1))
|
||||||
|
minutes = int(time_match.group(2))
|
||||||
|
seconds = int(time_match.group(3))
|
||||||
|
current_track["timestamp"] = timedelta(
|
||||||
|
hours=hours, minutes=minutes, seconds=seconds
|
||||||
|
)
|
||||||
|
tracks.append(
|
||||||
|
current_track.copy()
|
||||||
|
) # Ensure current track is added properly
|
||||||
|
current_track = {}
|
||||||
|
|
||||||
|
return update_episode_tracklist(episode, tracks)
|
||||||
|
|
||||||
|
|
||||||
# list of file processors
|
# list of file processors
|
||||||
|
@ -51,7 +117,10 @@ async def djuced_track_list(
|
||||||
# the second tuple item is the function to run which should return none if the file was not able to be processed, otherwise a mutated episode object
|
# the second tuple item is the function to run which should return none if the file was not able to be processed, otherwise a mutated episode object
|
||||||
processors: List[
|
processors: List[
|
||||||
Tuple[str, Callable[[PodcastEpisode, UploadFile], Optional[PodcastEpisode]]]
|
Tuple[str, Callable[[PodcastEpisode, UploadFile], Optional[PodcastEpisode]]]
|
||||||
] = [("text/xml", djuced_track_list)]
|
] = [
|
||||||
|
("text/xml", djuced_track_list),
|
||||||
|
("application/octet-stream", rekordbox_track_list),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def process_additional_episode_upload(
|
async def process_additional_episode_upload(
|
||||||
|
|
21
src/main.py
21
src/main.py
|
@ -142,20 +142,20 @@ def update_podcast_image(
|
||||||
|
|
||||||
if image is not None and image.size > 0:
|
if image is not None and image.size > 0:
|
||||||
if not (image.filename.endswith(".jpg") or image.filename.endswith(".png")):
|
if not (image.filename.endswith(".jpg") or image.filename.endswith(".png")):
|
||||||
return HTTPException(
|
raise HTTPException(
|
||||||
400, detail="The uploaded podcast image must be a jpg or png"
|
400, detail="The uploaded podcast image must be a jpg or png"
|
||||||
)
|
)
|
||||||
|
|
||||||
im = Image.open(image.file)
|
im = Image.open(image.file)
|
||||||
|
|
||||||
if im.size[0] != im.size[1] or im.size[0] < 1400 or im.size[0] > 3000:
|
if im.size[0] != im.size[1] or im.size[0] < 1400 or im.size[0] > 3000:
|
||||||
return HTTPException(
|
raise HTTPException(
|
||||||
400,
|
400,
|
||||||
detail="The uploaded podcast image must be square and between 1400x1400px and 3000x3000px in size",
|
detail="The uploaded podcast image must be square and between 1400x1400px and 3000x3000px in size",
|
||||||
)
|
)
|
||||||
|
|
||||||
if im.mode != "RGB":
|
if im.mode != "RGB":
|
||||||
return HTTPException(
|
raise HTTPException(
|
||||||
400, detail="The uploaded podcast image must be in RGB format"
|
400, detail="The uploaded podcast image must be in RGB format"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -429,7 +429,7 @@ def get_podcast_feed(session: SessionDep, request: Request, podcast_id: str):
|
||||||
description=podcast.description,
|
description=podcast.description,
|
||||||
image=urllib.parse.urljoin(
|
image=urllib.parse.urljoin(
|
||||||
str(request.base_url),
|
str(request.base_url),
|
||||||
f"/{podcast.id}/{podcast.image_filename}",
|
f"/files/{podcast.id}/{podcast.image_filename}",
|
||||||
)
|
)
|
||||||
if podcast.image_filename is not None
|
if podcast.image_filename is not None
|
||||||
else None,
|
else None,
|
||||||
|
@ -446,7 +446,7 @@ def get_podcast_feed(session: SessionDep, request: Request, podcast_id: str):
|
||||||
publication_date=episode.publish_date.astimezone(tz=timezone.utc),
|
publication_date=episode.publish_date.astimezone(tz=timezone.utc),
|
||||||
media=podgen.Media(
|
media=podgen.Media(
|
||||||
urllib.parse.urljoin(
|
urllib.parse.urljoin(
|
||||||
str(request.base_url), f"{podcast.id}/{episode.id}.m4a"
|
str(request.base_url), f"/files/{podcast.id}/{episode.id}.m4a"
|
||||||
),
|
),
|
||||||
episode.file_size,
|
episode.file_size,
|
||||||
duration=timedelta(seconds=episode.duration)
|
duration=timedelta(seconds=episode.duration)
|
||||||
|
@ -460,7 +460,7 @@ def get_podcast_feed(session: SessionDep, request: Request, podcast_id: str):
|
||||||
return Response(content=feed.rss_str(), media_type="application/xml")
|
return Response(content=feed.rss_str(), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{podcast_id}/{filename}")
|
@app.get("/files/{podcast_id}/{filename}")
|
||||||
def get_episode_or_cover(session: SessionDep, podcast_id: str, filename: str):
|
def get_episode_or_cover(session: SessionDep, podcast_id: str, filename: str):
|
||||||
podcast = session.exec(
|
podcast = session.exec(
|
||||||
select(models.Podcast).where(models.Podcast.id == podcast_id)
|
select(models.Podcast).where(models.Podcast.id == podcast_id)
|
||||||
|
@ -489,12 +489,15 @@ def get_episode_or_cover(session: SessionDep, podcast_id: str, filename: str):
|
||||||
) and filename == podcast.image_filename:
|
) and filename == podcast.image_filename:
|
||||||
return FileResponse(settings.directory / podcast.image_filename)
|
return FileResponse(settings.directory / podcast.image_filename)
|
||||||
|
|
||||||
return HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{catchall:path}")
|
@app.get("/{full_path:path}")
|
||||||
async def serve_app(catchall: str):
|
async def serve_app(full_path: str):
|
||||||
|
if not full_path.startswith("api/"):
|
||||||
return FileResponse("dist/index.html")
|
return FileResponse("dist/index.html")
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
|
||||||
use_route_names_as_operation_ids(app)
|
use_route_names_as_operation_ids(app)
|
||||||
|
|
Loading…
Reference in a new issue