forked from platypush/platypush
parent
9ed7026aaf
commit
d617443af6
7 changed files with 529 additions and 12 deletions
|
@ -63,7 +63,7 @@ nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 2.5px 0 4.5px 2px $nav-collapsed-fg;
|
box-shadow: 2.5px 0 4.5px 2px $nav-collapsed-fg;
|
||||||
margin-left: 2.5px;
|
margin-left: 2.5px;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
|
|
||||||
.menu-button {
|
.menu-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
@play="$emit('play', $event)"
|
@play="$emit('play', $event)"
|
||||||
@select="onPlaylistSelected"
|
@select="onPlaylistSelected"
|
||||||
v-else-if="selectedView === 'playlists'" />
|
v-else-if="selectedView === 'playlists'" />
|
||||||
|
<Subscriptions :filter="filter"
|
||||||
|
:selected-channel="selectedChannel"
|
||||||
|
@play="$emit('play', $event)"
|
||||||
|
@select="onChannelSelected"
|
||||||
|
v-else-if="selectedView === 'subscriptions'" />
|
||||||
<Index @select="selectView" v-else />
|
<Index @select="selectView" v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,6 +34,7 @@ import Feed from "./YouTube/Feed";
|
||||||
import Index from "./YouTube/Index";
|
import Index from "./YouTube/Index";
|
||||||
import NoToken from "./YouTube/NoToken";
|
import NoToken from "./YouTube/NoToken";
|
||||||
import Playlists from "./YouTube/Playlists";
|
import Playlists from "./YouTube/Playlists";
|
||||||
|
import Subscriptions from "./YouTube/Subscriptions";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [MediaProvider],
|
mixins: [MediaProvider],
|
||||||
|
@ -39,6 +45,7 @@ export default {
|
||||||
MediaNav,
|
MediaNav,
|
||||||
NoToken,
|
NoToken,
|
||||||
Playlists,
|
Playlists,
|
||||||
|
Subscriptions,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -46,6 +53,7 @@ export default {
|
||||||
youtubeConfig: null,
|
youtubeConfig: null,
|
||||||
selectedView: null,
|
selectedView: null,
|
||||||
selectedPlaylist: null,
|
selectedPlaylist: null,
|
||||||
|
selectedChannel: null,
|
||||||
path: [],
|
path: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -83,6 +91,8 @@ export default {
|
||||||
this.selectedView = view
|
this.selectedView = view
|
||||||
if (view === 'playlists')
|
if (view === 'playlists')
|
||||||
this.selectedPlaylist = null
|
this.selectedPlaylist = null
|
||||||
|
else if (view === 'subscriptions')
|
||||||
|
this.selectedChannel = null
|
||||||
|
|
||||||
if (view?.length) {
|
if (view?.length) {
|
||||||
this.path = [
|
this.path = [
|
||||||
|
@ -102,6 +112,13 @@ export default {
|
||||||
title: playlist.name,
|
title: playlist.name,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onChannelSelected(channel) {
|
||||||
|
this.selectedChannel = channel.id
|
||||||
|
this.path.push({
|
||||||
|
title: channel.name,
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
<template>
|
||||||
|
<div class="media-youtube-channel" @scroll="onScroll">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
|
<div class="channel" @scroll="onScroll" v-else-if="channel">
|
||||||
|
<div class="header">
|
||||||
|
<div class="banner">
|
||||||
|
<img :src="channel.banner" v-if="channel?.banner?.length" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<a :href="channel.url" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="image">
|
||||||
|
<img :src="channel.image" v-if="channel?.image?.length" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<a class="title" :href="channel.url" target="_blank" rel="noopener noreferrer">
|
||||||
|
{{ channel?.name }}
|
||||||
|
</a>
|
||||||
|
<div class="description">{{ channel?.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Results :results="channel.items"
|
||||||
|
:filter="filter"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
ref="results"
|
||||||
|
@select="selectedResult = $event"
|
||||||
|
@play="$emit('play', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Results from "@/components/panels/Media/Results";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['play'],
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
Results,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
channel: null,
|
||||||
|
loading: false,
|
||||||
|
loadingNextPage: false,
|
||||||
|
selectedResult: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
itemsByUrl() {
|
||||||
|
return this.channel?.items.reduce((acc, item) => {
|
||||||
|
acc[item.url] = item
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadChannel() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.channel = await this.request('youtube.get_channel', {id: this.id})
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadNextPage() {
|
||||||
|
if (!this.channel?.next_page_token || this.loadingNextPage)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextPage = await this.request(
|
||||||
|
'youtube.get_channel',
|
||||||
|
{id: this.id, next_page_token: this.channel.next_page_token}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.channel.items.push(...nextPage.items.filter(item => !this.itemsByUrl[item.url]))
|
||||||
|
this.channel.next_page_token = nextPage.next_page_token
|
||||||
|
this.$refs.results.maxResultIndex += this.$refs.results.resultIndexStep
|
||||||
|
} finally {
|
||||||
|
this.loadingNextPage = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onScroll(e) {
|
||||||
|
const el = e.target
|
||||||
|
if (!el)
|
||||||
|
return
|
||||||
|
|
||||||
|
const bottom = (el.scrollHeight - el.scrollTop) <= el.clientHeight + 150
|
||||||
|
if (!bottom)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.loadNextPage()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadChannel()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.media-youtube-channel {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: $default-border-2;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
max-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 100px;
|
||||||
|
margin: -2.5em 2em 0.5em 0.5em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@include from($desktop) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $default-fg-2;
|
||||||
|
font-size: 1.7em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
text-decoration: dotted;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div class="media-youtube-subscriptions">
|
||||||
|
<div class="subscriptions-index" v-if="!selectedChannel">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<NoItems :with-shadow="false" v-else-if="!channels?.length">
|
||||||
|
No channels found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<div class="body grid" v-else>
|
||||||
|
<div class="channel item"
|
||||||
|
v-for="(channel, id) in channelsById"
|
||||||
|
:key="id"
|
||||||
|
@click="$emit('select', channel)">
|
||||||
|
<div class="image">
|
||||||
|
<img :src="channel.image" :alt="channel.name" />
|
||||||
|
</div>
|
||||||
|
<div class="title">{{ channel.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-body" v-else>
|
||||||
|
<Channel :id="selectedChannel" :filter="filter" @play="$emit('play', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Channel from "./Channel";
|
||||||
|
import NoItems from "@/components/elements/NoItems";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['play', 'select'],
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {
|
||||||
|
Channel,
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
selectedChannel: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
channels: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
channelsById() {
|
||||||
|
return this.channels
|
||||||
|
.filter(channel => !this.filter || channel.name.toLowerCase().includes(this.filter.toLowerCase()))
|
||||||
|
.reduce((acc, channel) => {
|
||||||
|
acc[channel.id] = channel
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadSubscriptions() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.channels = (await this.request('youtube.get_subscriptions'))
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadSubscriptions()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.media-youtube-subscriptions {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.channel.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: $default-border-2;
|
||||||
|
box-shadow: $border-shadow-bottom;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 10em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
transition: filter 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: contrast(70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,16 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-results" @scroll="onScroll">
|
<div class="media-results" @scroll="onScroll">
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<NoItems v-else-if="!results?.length" :with-shadow="false">
|
<div class="grid" ref="grid" v-if="results?.length">
|
||||||
No search results
|
|
||||||
</NoItems>
|
|
||||||
|
|
||||||
<div class="grid" ref="grid" v-else>
|
|
||||||
<Item v-for="(item, i) in visibleResults"
|
<Item v-for="(item, i) in visibleResults"
|
||||||
:key="i"
|
:key="i"
|
||||||
:item="item"
|
:item="item"
|
||||||
:selected="selectedResult === i"
|
:selected="selectedResult === i"
|
||||||
:hidden="!sources[item.type]"
|
:hidden="!!Object.keys(sources || {}).length && !sources[item.type]"
|
||||||
@select="$emit('select', i)"
|
@select="$emit('select', i)"
|
||||||
@play="$emit('play', item)"
|
@play="$emit('play', item)"
|
||||||
@view="$emit('view', item)"
|
@view="$emit('view', item)"
|
||||||
|
@ -30,11 +26,10 @@ import Info from "@/components/panels/Media/Info";
|
||||||
import Item from "./Item";
|
import Item from "./Item";
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import NoItems from "@/components/elements/NoItems";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {Info, Item, Loading, Modal, NoItems},
|
components: {Info, Item, Loading, Modal},
|
||||||
emits: ['select', 'play', 'view', 'download'],
|
emits: ['select', 'play', 'view', 'download', 'scroll-end'],
|
||||||
props: {
|
props: {
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -95,6 +90,7 @@ export default {
|
||||||
if (!bottom)
|
if (!bottom)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
this.$emit('scroll-end')
|
||||||
this.maxResultIndex += this.resultIndexStep
|
this.maxResultIndex += this.resultIndexStep
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import base64
|
||||||
|
from functools import lru_cache
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
from platypush.schemas.piped import PipedPlaylistSchema, PipedVideoSchema
|
from platypush.schemas.piped import (
|
||||||
|
PipedChannelSchema,
|
||||||
|
PipedPlaylistSchema,
|
||||||
|
PipedVideoSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlugin(Plugin):
|
class YoutubePlugin(Plugin):
|
||||||
|
@ -53,7 +59,9 @@ class YoutubePlugin(Plugin):
|
||||||
def _api_url(self, path: str = '') -> str:
|
def _api_url(self, path: str = '') -> str:
|
||||||
return f"{self._piped_api_url}/{path}"
|
return f"{self._piped_api_url}/{path}"
|
||||||
|
|
||||||
def _request(self, path: str, auth: bool = True, **kwargs):
|
def _request(
|
||||||
|
self, path: str, body: Optional[str] = None, auth: bool = True, **kwargs
|
||||||
|
):
|
||||||
timeout = kwargs.pop('timeout', self._timeout)
|
timeout = kwargs.pop('timeout', self._timeout)
|
||||||
if auth:
|
if auth:
|
||||||
kwargs['params'] = kwargs.get('params', {})
|
kwargs['params'] = kwargs.get('params', {})
|
||||||
|
@ -61,10 +69,26 @@ class YoutubePlugin(Plugin):
|
||||||
kwargs['headers'] = kwargs.get('headers', {})
|
kwargs['headers'] = kwargs.get('headers', {})
|
||||||
kwargs['headers']['Authorization'] = self._auth_token
|
kwargs['headers']['Authorization'] = self._auth_token
|
||||||
|
|
||||||
|
if body:
|
||||||
|
kwargs['data'] = body
|
||||||
|
|
||||||
rs = requests.get(self._api_url(path), timeout=timeout, **kwargs)
|
rs = requests.get(self._api_url(path), timeout=timeout, **kwargs)
|
||||||
rs.raise_for_status()
|
rs.raise_for_status()
|
||||||
return rs.json()
|
return rs.json()
|
||||||
|
|
||||||
|
@lru_cache(maxsize=10) # noqa
|
||||||
|
def _get_channel(self, id: str) -> dict: # pylint: disable=redefined-builtin
|
||||||
|
if (
|
||||||
|
id.startswith('http')
|
||||||
|
or id.startswith('https')
|
||||||
|
or id.startswith('/channel/')
|
||||||
|
):
|
||||||
|
id = id.split('/')[-1]
|
||||||
|
|
||||||
|
return (
|
||||||
|
PipedChannelSchema().dump(self._request(f'channel/{id}')) or {} # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def search(self, query: str, **_) -> List[dict]:
|
def search(self, query: str, **_) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
|
@ -124,5 +148,53 @@ class YoutubePlugin(Plugin):
|
||||||
or []
|
or []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_subscriptions(self) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Retrieve the channels subscribed by the user logged in to the Piped
|
||||||
|
instance.
|
||||||
|
|
||||||
|
:return: .. schema:: piped.PipedChannelSchema(many=True)
|
||||||
|
"""
|
||||||
|
return PipedChannelSchema(many=True).dump(self._request('subscriptions')) or []
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_channel(
|
||||||
|
self,
|
||||||
|
id: str, # pylint: disable=redefined-builtin
|
||||||
|
next_page_token: Optional[str] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Retrieve the information and videos of a channel given its ID or URL.
|
||||||
|
|
||||||
|
:param id: Channel ID or URL.
|
||||||
|
:param next_page_token: Optional token to retrieve the next page of
|
||||||
|
results.
|
||||||
|
:return: .. schema:: piped.PipedChannelSchema
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
id.startswith('http')
|
||||||
|
or id.startswith('https')
|
||||||
|
or id.startswith('/channel/')
|
||||||
|
):
|
||||||
|
id = id.split('/')[-1]
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
if next_page_token:
|
||||||
|
info = self._get_channel(id).copy()
|
||||||
|
info.pop('next_page_token', None)
|
||||||
|
info['items'] = []
|
||||||
|
next_page = base64.b64decode(next_page_token.encode()).decode()
|
||||||
|
response = {
|
||||||
|
**info,
|
||||||
|
**self._request(
|
||||||
|
f'nextpage/channel/{id}', params={'nextpage': next_page}, auth=False
|
||||||
|
),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = self._request(f'channel/{id}')
|
||||||
|
|
||||||
|
return PipedChannelSchema().dump(response) or {} # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from marshmallow import EXCLUDE, fields, pre_dump
|
from marshmallow import EXCLUDE, fields, pre_dump
|
||||||
|
@ -157,3 +158,109 @@ class PipedPlaylistSchema(Schema):
|
||||||
'example': 10,
|
'example': 10,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PipedChannelSchema(Schema):
|
||||||
|
"""
|
||||||
|
Class for channel items returned by the Piped API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Exclude unknown fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
id = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel ID',
|
||||||
|
'example': '1234567890',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
url = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel URL',
|
||||||
|
'example': 'https://youtube.com/channel/1234567890',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
name = StrippedString(
|
||||||
|
missing='[No Name]',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel name',
|
||||||
|
'example': 'My Channel Name',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
description = StrippedString(
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel description',
|
||||||
|
'example': 'My channel description',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
image = fields.Url(
|
||||||
|
attribute='avatar',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel image URL',
|
||||||
|
'example': 'https://i.ytimg.com/vi/1234567890/hqdefault.jpg',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
banner = fields.Url(
|
||||||
|
attribute='bannerUrl',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel banner URL',
|
||||||
|
'example': 'https://i.ytimg.com/vi/1234567890/hqdefault.jpg',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribers = fields.Int(
|
||||||
|
attribute='subscriberCount',
|
||||||
|
missing=0,
|
||||||
|
metadata={
|
||||||
|
'description': 'Number of subscribers',
|
||||||
|
'example': 1000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
next_page_token = fields.String(
|
||||||
|
attribute='nextpage',
|
||||||
|
metadata={
|
||||||
|
'description': 'The token that should be passed to get the next page of results',
|
||||||
|
'example': '1234567890',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
items = fields.Nested(PipedVideoSchema, attribute='relatedStreams', many=True)
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def normalize_id_and_url(self, data: dict, **_):
|
||||||
|
if data.get('id'):
|
||||||
|
if not data.get('url'):
|
||||||
|
data['url'] = f'https://youtube.com/channel/{data["id"]}'
|
||||||
|
elif data.get('url'):
|
||||||
|
data['id'] = data['url'].split('/')[-1]
|
||||||
|
data['url'] = f'https://youtube.com{data["url"]}'
|
||||||
|
else:
|
||||||
|
raise AssertionError('Channel ID or URL not found')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def normalize_avatar(self, data: dict, **_):
|
||||||
|
if data.get('avatarUrl'):
|
||||||
|
data['avatar'] = data.pop('avatarUrl')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def serialize_next_page_token(self, data: dict, **_):
|
||||||
|
if data.get('nextpage'):
|
||||||
|
data['nextpage'] = base64.b64encode(data['nextpage'].encode()).decode()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
Loading…
Reference in a new issue