mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 19:11:49 +01:00
add id's to portables for better seasons
This commit is contained in:
parent
9b47f81afb
commit
570ca14905
@ -4,7 +4,7 @@ import React, { Fragment } from "react";
|
|||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
export interface OptionItem {
|
export interface OptionItem {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Dropdown } from "components/Dropdown";
|
import { Dropdown, OptionItem } from "components/Dropdown";
|
||||||
import { WatchedEpisode } from "components/media/WatchedEpisodeButton";
|
import { WatchedEpisode } from "components/media/WatchedEpisodeButton";
|
||||||
import { useLoading } from "hooks/useLoading";
|
import { useLoading } from "hooks/useLoading";
|
||||||
import { serializePortableMedia } from "hooks/usePortableMedia";
|
import { serializePortableMedia } from "hooks/usePortableMedia";
|
||||||
@ -6,6 +6,7 @@ import {
|
|||||||
convertMediaToPortable,
|
convertMediaToPortable,
|
||||||
MWMedia,
|
MWMedia,
|
||||||
MWMediaSeasons,
|
MWMediaSeasons,
|
||||||
|
MWMediaSeason,
|
||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
} from "providers";
|
} from "providers";
|
||||||
import { getSeasonDataFromMedia } from "providers/methods/seasons";
|
import { getSeasonDataFromMedia } from "providers/methods/seasons";
|
||||||
@ -22,8 +23,8 @@ export function Seasons(props: SeasonsProps) {
|
|||||||
);
|
);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
|
const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
|
||||||
const seasonSelected = props.media.season as number;
|
const seasonSelected = props.media.seasonId as string;
|
||||||
const episodeSelected = props.media.episode as number;
|
const episodeSelected = props.media.episodeId as string;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -32,10 +33,10 @@ export function Seasons(props: SeasonsProps) {
|
|||||||
})();
|
})();
|
||||||
}, [searchSeasons, props.media]);
|
}, [searchSeasons, props.media]);
|
||||||
|
|
||||||
function navigateToSeasonAndEpisode(season: number, episode: number) {
|
function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
|
||||||
const newMedia: MWMedia = { ...props.media };
|
const newMedia: MWMedia = { ...props.media };
|
||||||
newMedia.episode = episode;
|
newMedia.episodeId = episodeId;
|
||||||
newMedia.season = season;
|
newMedia.seasonId = seasonId;
|
||||||
history.replace(
|
history.replace(
|
||||||
`/media/${newMedia.mediaType}/${serializePortableMedia(
|
`/media/${newMedia.mediaType}/${serializePortableMedia(
|
||||||
convertMediaToPortable(newMedia)
|
convertMediaToPortable(newMedia)
|
||||||
@ -43,15 +44,17 @@ export function Seasons(props: SeasonsProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = seasons.seasons.map((season) => ({
|
const mapSeason = (season: MWMediaSeason) => ({
|
||||||
id: season.seasonNumber,
|
id: season.id,
|
||||||
name: `Season ${season.seasonNumber}`,
|
name: season.title || `Season ${season.sort}`,
|
||||||
}));
|
});
|
||||||
|
|
||||||
const selectedItem = {
|
const options = seasons.seasons.map(mapSeason);
|
||||||
id: seasonSelected,
|
|
||||||
name: `Season ${seasonSelected}`,
|
const foundSeason = seasons.seasons.find(
|
||||||
};
|
(season) => season.id === seasonSelected
|
||||||
|
);
|
||||||
|
const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -60,29 +63,31 @@ export function Seasons(props: SeasonsProps) {
|
|||||||
{success && seasons.seasons.length ? (
|
{success && seasons.seasons.length ? (
|
||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem as OptionItem}
|
||||||
options={options}
|
options={options}
|
||||||
setSelectedItem={(seasonItem) =>
|
setSelectedItem={(seasonItem) =>
|
||||||
navigateToSeasonAndEpisode(
|
navigateToSeasonAndEpisode(
|
||||||
seasonItem.id,
|
seasonItem.id,
|
||||||
seasons.seasons[seasonItem.id]?.episodes[0].episodeNumber
|
seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
|
||||||
|
.id as string
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{seasons.seasons[seasonSelected]?.episodes.map((v) => (
|
{seasons.seasons
|
||||||
<WatchedEpisode
|
.find((s) => s.id === seasonSelected)
|
||||||
key={v.episodeNumber}
|
?.episodes.map((v) => (
|
||||||
media={{
|
<WatchedEpisode
|
||||||
...props.media,
|
key={v.id}
|
||||||
episode: v.episodeNumber,
|
media={{
|
||||||
season: seasonSelected,
|
...props.media,
|
||||||
}}
|
seriesData: seasons,
|
||||||
active={v.episodeNumber === episodeSelected}
|
episodeId: v.id,
|
||||||
onClick={() =>
|
seasonId: seasonSelected,
|
||||||
navigateToSeasonAndEpisode(seasonSelected, v.episodeNumber)
|
}}
|
||||||
}
|
active={v.id === episodeSelected}
|
||||||
/>
|
onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
convertMediaToPortable,
|
convertMediaToPortable,
|
||||||
|
getEpisodeFromMedia,
|
||||||
getProviderFromId,
|
getProviderFromId,
|
||||||
MWMediaMeta,
|
MWMediaMeta,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
@ -53,9 +54,9 @@ function MediaCardContent({
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="mb-1 font-bold text-white">
|
<h1 className="mb-1 font-bold text-white">
|
||||||
{media.title}
|
{media.title}
|
||||||
{series ? (
|
{series && media.seasonId && media.episodeId ? (
|
||||||
<span className="text-denim-700 ml-2 text-xs">
|
<span className="text-denim-700 ml-2 text-xs">
|
||||||
S{media.season} E{media.episode}
|
S{media.seasonId} E{media.episodeId}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { MWMediaMeta } from "providers";
|
import { getEpisodeFromMedia, MWMedia } from "providers";
|
||||||
import { useWatchedContext, getWatchedFromPortable } from "state/watched";
|
import { useWatchedContext, getWatchedFromPortable } from "state/watched";
|
||||||
import { Episode } from "./EpisodeButton";
|
import { Episode } from "./EpisodeButton";
|
||||||
|
|
||||||
export interface WatchedEpisodeProps {
|
export interface WatchedEpisodeProps {
|
||||||
media: MWMediaMeta;
|
media: MWMedia;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
@ -11,12 +11,13 @@ export interface WatchedEpisodeProps {
|
|||||||
export function WatchedEpisode(props: WatchedEpisodeProps) {
|
export function WatchedEpisode(props: WatchedEpisodeProps) {
|
||||||
const { watched } = useWatchedContext();
|
const { watched } = useWatchedContext();
|
||||||
const foundWatched = getWatchedFromPortable(watched.items, props.media);
|
const foundWatched = getWatchedFromPortable(watched.items, props.media);
|
||||||
|
const episode = getEpisodeFromMedia(props.media);
|
||||||
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
|
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Episode
|
<Episode
|
||||||
progress={watchedPercentage}
|
progress={watchedPercentage}
|
||||||
episodeNumber={props.media.episode ?? 1}
|
episodeNumber={episode?.episode?.sort ?? 1}
|
||||||
active={props.active}
|
active={props.active}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
/>
|
/>
|
||||||
|
@ -16,7 +16,7 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
|||||||
<MediaCard
|
<MediaCard
|
||||||
watchedPercentage={watchedPercentage}
|
watchedPercentage={watchedPercentage}
|
||||||
media={props.media}
|
media={props.media}
|
||||||
series={props.series && props.media.episode !== undefined}
|
series={props.series && props.media.episodeId !== undefined}
|
||||||
linkable
|
linkable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -14,8 +14,8 @@ export function convertMediaToPortable(media: MWMedia): MWPortableMedia {
|
|||||||
mediaId: media.mediaId,
|
mediaId: media.mediaId,
|
||||||
providerId: media.providerId,
|
providerId: media.providerId,
|
||||||
mediaType: media.mediaType,
|
mediaType: media.mediaType,
|
||||||
episode: media.episode,
|
episodeId: media.episodeId,
|
||||||
season: media.season,
|
seasonId: media.seasonId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export const theFlixScraper: MWMediaProvider = {
|
|||||||
if (media.mediaType === MWMediaType.MOVIE) {
|
if (media.mediaType === MWMediaType.MOVIE) {
|
||||||
url = `${CORS_PROXY_URL}https://theflix.to/movie/${media.mediaId}?movieInfo=${media.mediaId}`;
|
url = `${CORS_PROXY_URL}https://theflix.to/movie/${media.mediaId}?movieInfo=${media.mediaId}`;
|
||||||
} else if (media.mediaType === MWMediaType.SERIES) {
|
} else if (media.mediaType === MWMediaType.SERIES) {
|
||||||
url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`;
|
url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url).then((d) => d.text());
|
const res = await fetch(url).then((d) => d.text());
|
||||||
@ -75,7 +75,7 @@ export const theFlixScraper: MWMediaProvider = {
|
|||||||
async getSeasonDataFromMedia(
|
async getSeasonDataFromMedia(
|
||||||
media: MWPortableMedia
|
media: MWPortableMedia
|
||||||
): Promise<MWMediaSeasons> {
|
): Promise<MWMediaSeasons> {
|
||||||
const url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`;
|
const url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
|
||||||
const res = await fetch(url).then((d) => d.text());
|
const res = await fetch(url).then((d) => d.text());
|
||||||
|
|
||||||
const node: Element = Array.from(
|
const node: Element = Array.from(
|
||||||
@ -87,10 +87,14 @@ export const theFlixScraper: MWMediaProvider = {
|
|||||||
const data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons;
|
const data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons;
|
||||||
return {
|
return {
|
||||||
seasons: data.map((d: any) => ({
|
seasons: data.map((d: any) => ({
|
||||||
seasonNumber: d.seasonNumber === 0 ? 999 : d.seasonNumber,
|
sort: d.seasonNumber === 0 ? 999 : d.seasonNumber,
|
||||||
|
id: d.seasonNumber.toString(),
|
||||||
type: d.seasonNumber === 0 ? "special" : "season",
|
type: d.seasonNumber === 0 ? "special" : "season",
|
||||||
|
title: d.seasonNumber === 0 ? "Specials" : undefined,
|
||||||
episodes: d.episodes.map((e: any) => ({
|
episodes: d.episodes.map((e: any) => ({
|
||||||
title: e.name,
|
title: e.name,
|
||||||
|
sort: e.episodeNumber,
|
||||||
|
id: e.episodeNumber.toString(),
|
||||||
episodeNumber: e.episodeNumber,
|
episodeNumber: e.episodeNumber,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
|
@ -6,7 +6,7 @@ const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
|
|||||||
return `https://theflix.to/movie/${media.mediaId}?${params}`;
|
return `https://theflix.to/movie/${media.mediaId}?${params}`;
|
||||||
}
|
}
|
||||||
if (media.mediaType === MWMediaType.SERIES) {
|
if (media.mediaType === MWMediaType.SERIES) {
|
||||||
return `https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`;
|
return `https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
@ -38,11 +38,11 @@ export function turnDataIntoMedia(data: any): MWProviderMediaResult {
|
|||||||
title: data.name,
|
title: data.name,
|
||||||
year: new Date(data.releaseDate).getFullYear().toString(),
|
year: new Date(data.releaseDate).getFullYear().toString(),
|
||||||
seasonCount: data.numberOfSeasons,
|
seasonCount: data.numberOfSeasons,
|
||||||
episode: data.lastReleasedEpisode
|
episodeId: data.lastReleasedEpisode
|
||||||
? data.lastReleasedEpisode.episodeNumber
|
? data.lastReleasedEpisode.episodeNumber.toString()
|
||||||
: null,
|
: null,
|
||||||
season: data.lastReleasedEpisode
|
seasonId: data.lastReleasedEpisode
|
||||||
? data.lastReleasedEpisode.seasonNumber
|
? data.lastReleasedEpisode.seasonNumber.toString()
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MWMediaType, MWMediaProviderMetadata } from "providers";
|
import { MWMediaType, MWMediaProviderMetadata } from "providers";
|
||||||
|
import { MWMedia, MWMediaEpisode, MWMediaSeason } from "providers/types";
|
||||||
import { mediaProviders, mediaProvidersUnchecked } from "./providers";
|
import { mediaProviders, mediaProvidersUnchecked } from "./providers";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -38,3 +39,27 @@ export function getProviderMetadata(id: string): MWMediaProviderMetadata {
|
|||||||
provider,
|
provider,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
** get episode and season from media
|
||||||
|
*/
|
||||||
|
export function getEpisodeFromMedia(
|
||||||
|
media: MWMedia
|
||||||
|
): { season: MWMediaSeason; episode: MWMediaEpisode } | null {
|
||||||
|
if (
|
||||||
|
media.seasonId === undefined ||
|
||||||
|
media.episodeId === undefined ||
|
||||||
|
media.seriesData === undefined
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const season = media.seriesData.seasons.find((v) => v.id === media.seasonId);
|
||||||
|
if (!season) return null;
|
||||||
|
const episode = season?.episodes.find((v) => v.id === media.episodeId);
|
||||||
|
if (!episode) return null;
|
||||||
|
return {
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -28,10 +28,8 @@ export async function getSeasonDataFromMedia(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seasonData = await provider.getSeasonDataFromMedia(media);
|
const seasonData = await provider.getSeasonDataFromMedia(media);
|
||||||
seasonData.seasons.sort((a, b) => a.seasonNumber - b.seasonNumber);
|
seasonData.seasons.sort((a, b) => a.sort - b.sort);
|
||||||
seasonData.seasons.forEach((s) =>
|
seasonData.seasons.forEach((s) => s.episodes.sort((a, b) => a.sort - b.sort));
|
||||||
s.episodes.sort((a, b) => a.episodeNumber - b.episodeNumber)
|
|
||||||
);
|
|
||||||
|
|
||||||
// cache it
|
// cache it
|
||||||
seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour
|
seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour
|
||||||
|
@ -8,8 +8,8 @@ export interface MWPortableMedia {
|
|||||||
mediaId: string;
|
mediaId: string;
|
||||||
mediaType: MWMediaType;
|
mediaType: MWMediaType;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
season?: number;
|
seasonId?: string;
|
||||||
episode?: number;
|
episodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MWMediaStreamType = "m3u8" | "mp4";
|
export type MWMediaStreamType = "m3u8" | "mp4";
|
||||||
@ -24,15 +24,20 @@ export interface MWMediaMeta extends MWPortableMedia {
|
|||||||
seasonCount?: number;
|
seasonCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MWMediaEpisode {
|
||||||
|
sort: number;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
export interface MWMediaSeason {
|
||||||
|
sort: number;
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
type: "season" | "special";
|
||||||
|
episodes: MWMediaEpisode[];
|
||||||
|
}
|
||||||
export interface MWMediaSeasons {
|
export interface MWMediaSeasons {
|
||||||
seasons: {
|
seasons: MWMediaSeason[];
|
||||||
seasonNumber: number;
|
|
||||||
type: "season" | "special";
|
|
||||||
episodes: {
|
|
||||||
title: string;
|
|
||||||
episodeNumber: number;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MWMedia extends MWMediaMeta {
|
export interface MWMedia extends MWMediaMeta {
|
||||||
|
@ -23,8 +23,8 @@ export function WrapProvider(
|
|||||||
// consult cache first
|
// consult cache first
|
||||||
const output = contentCache.get(media);
|
const output = contentCache.get(media);
|
||||||
if (output) {
|
if (output) {
|
||||||
output.season = media.season;
|
output.seasonId = media.seasonId;
|
||||||
output.episode = media.episode;
|
output.episodeId = media.episodeId;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ function getBookmarkIndexFromMedia(
|
|||||||
(v) =>
|
(v) =>
|
||||||
v.mediaId === media.mediaId &&
|
v.mediaId === media.mediaId &&
|
||||||
v.providerId === media.providerId &&
|
v.providerId === media.providerId &&
|
||||||
v.episode === media.episode &&
|
v.episodeId === media.episodeId &&
|
||||||
v.season === media.season
|
v.seasonId === media.seasonId
|
||||||
);
|
);
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
@ -75,8 +75,8 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
|
|||||||
providerId: media.providerId,
|
providerId: media.providerId,
|
||||||
title: media.title,
|
title: media.title,
|
||||||
year: media.year,
|
year: media.year,
|
||||||
episode: media.episode,
|
episodeId: media.episodeId,
|
||||||
season: media.season,
|
seasonId: media.seasonId,
|
||||||
};
|
};
|
||||||
data.bookmarks.push(item);
|
data.bookmarks.push(item);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers";
|
import {
|
||||||
|
MWMediaMeta,
|
||||||
|
getProviderMetadata,
|
||||||
|
MWMediaType,
|
||||||
|
getEpisodeFromMedia,
|
||||||
|
} from "providers";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -32,8 +37,8 @@ export function getWatchedFromPortable(
|
|||||||
(v) =>
|
(v) =>
|
||||||
v.mediaId === media.mediaId &&
|
v.mediaId === media.mediaId &&
|
||||||
v.providerId === media.providerId &&
|
v.providerId === media.providerId &&
|
||||||
v.episode === media.episode &&
|
v.episodeId === media.episodeId &&
|
||||||
v.season === media.season
|
v.seasonId === media.seasonId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +89,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||||||
year: media.year,
|
year: media.year,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
episode: media.episode,
|
episodeId: media.episodeId,
|
||||||
season: media.season,
|
seasonId: media.seasonId,
|
||||||
};
|
};
|
||||||
data.items.push(item);
|
data.items.push(item);
|
||||||
}
|
}
|
||||||
@ -112,8 +117,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||||||
) {
|
) {
|
||||||
const key = `${item.mediaType}-${item.mediaId}`;
|
const key = `${item.mediaType}-${item.mediaId}`;
|
||||||
const current: [number, number] = [
|
const current: [number, number] = [
|
||||||
item.season ?? -1,
|
item.episodeId ? parseInt(item.episodeId, 10) : -1,
|
||||||
item.episode ?? -1,
|
item.seasonId ? parseInt(item.seasonId, 10) : -1,
|
||||||
];
|
];
|
||||||
let existing = highestEpisode[key];
|
let existing = highestEpisode[key];
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
|
Loading…
Reference in New Issue
Block a user