Small bug fixes and added time to objects.
This commit is contained in:
parent
10a31b5ee8
commit
b7bf9627a1
13 changed files with 82 additions and 26 deletions
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
|
@ -33,8 +33,7 @@
|
||||||
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
|
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
|
||||||
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
|
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
|
||||||
"YTP_YTDL_OPTIONS_FILE": "${workspaceFolder}/var/config/ytdlp.json",
|
"YTP_YTDL_OPTIONS_FILE": "${workspaceFolder}/var/config/ytdlp.json",
|
||||||
"YTP_URL_HOST": "http://localhost:8081",
|
"YTP_URL_HOST": "http://localhost:8081"
|
||||||
"YTP_KEEP_ARCHIVE": "true",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class Config:
|
||||||
|
|
||||||
ytdl_options: dict | str = {}
|
ytdl_options: dict | str = {}
|
||||||
ytdl_options_file: str = ''
|
ytdl_options_file: str = ''
|
||||||
|
ytdl_debug: bool = False
|
||||||
|
|
||||||
host: str = '0.0.0.0'
|
host: str = '0.0.0.0'
|
||||||
port: int = 8081
|
port: int = 8081
|
||||||
|
|
@ -31,7 +32,7 @@ class Config:
|
||||||
|
|
||||||
base_path: str = ''
|
base_path: str = ''
|
||||||
|
|
||||||
_boolean_vars: tuple = ('keep_archive')
|
_boolean_vars: tuple = ('keep_archive', 'ytdl_debug')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
baseDefualtPath: str = os.path.dirname(os.path.dirname(__file__))
|
baseDefualtPath: str = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class ItemDTO:
|
||||||
ytdlp_cookies: str = None
|
ytdlp_cookies: str = None
|
||||||
ytdlp_config: dict = field(default_factory=dict)
|
ytdlp_config: dict = field(default_factory=dict)
|
||||||
output_template: str = None
|
output_template: str = None
|
||||||
timestamp: int = time.time_ns()
|
timestamp: float = time.time_ns()
|
||||||
is_live: bool = None
|
is_live: bool = None
|
||||||
|
|
||||||
# yt-dlp injected fields.
|
# yt-dlp injected fields.
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class DataStore:
|
||||||
with sqlite3.connect(self.db_file) as db:
|
with sqlite3.connect(self.db_file) as db:
|
||||||
db.row_factory = sqlite3.Row
|
db.row_factory = sqlite3.Row
|
||||||
cursor = db.execute(
|
cursor = db.execute(
|
||||||
'SELECT "id", "data" FROM "history" WHERE "type" = ? ORDER BY "created_at" ASC',
|
f'SELECT "id", "data" FROM "history" WHERE "type" = ? ORDER BY "created_at" ASC',
|
||||||
(self.type,)
|
(self.type,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class Download:
|
||||||
ytdl_opts: dict = None
|
ytdl_opts: dict = None
|
||||||
info: ItemDTO = None
|
info: ItemDTO = None
|
||||||
default_ytdl_opts: dict = None
|
default_ytdl_opts: dict = None
|
||||||
|
debug: bool = False
|
||||||
|
|
||||||
_ytdlp_fields: tuple = (
|
_ytdlp_fields: tuple = (
|
||||||
'tmpfilename',
|
'tmpfilename',
|
||||||
|
|
@ -41,16 +42,19 @@ class Download:
|
||||||
download_dir: str,
|
download_dir: str,
|
||||||
temp_dir: str,
|
temp_dir: str,
|
||||||
output_template_chapter: str,
|
output_template_chapter: str,
|
||||||
default_ytdl_opts: dict
|
default_ytdl_opts: dict,
|
||||||
|
debug: bool = False
|
||||||
):
|
):
|
||||||
self.download_dir = download_dir
|
self.download_dir = download_dir
|
||||||
self.temp_dir = temp_dir
|
self.temp_dir = temp_dir
|
||||||
self.output_template_chapter = output_template_chapter
|
self.output_template_chapter = output_template_chapter
|
||||||
self.output_template = info.output_template
|
self.output_template = info.output_template
|
||||||
self.format = get_format(info.format, info.quality)
|
self.format = get_format(info.format, info.quality)
|
||||||
self.ytdl_opts = get_opts(info.format, info.quality, info.ytdlp_config if info.ytdlp_config else {})
|
self.ytdl_opts = get_opts(
|
||||||
|
info.format, info.quality, info.ytdlp_config if info.ytdlp_config else {})
|
||||||
self.info = info
|
self.info = info
|
||||||
self.default_ytdl_opts = default_ytdl_opts
|
self.default_ytdl_opts = default_ytdl_opts
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
self.tmpfilename = None
|
self.tmpfilename = None
|
||||||
|
|
@ -82,7 +86,6 @@ class Download:
|
||||||
)
|
)
|
||||||
|
|
||||||
params: dict = {
|
params: dict = {
|
||||||
'quiet': True,
|
|
||||||
'no_color': True,
|
'no_color': True,
|
||||||
'format': self.format,
|
'format': self.format,
|
||||||
'paths': {
|
'paths': {
|
||||||
|
|
@ -99,6 +102,12 @@ class Download:
|
||||||
**mergeConfig(self.default_ytdl_opts, self.ytdl_opts),
|
**mergeConfig(self.default_ytdl_opts, self.ytdl_opts),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
params['verbose'] = True
|
||||||
|
params['logger'] = logging.getLogger('YTPTube-ytdl')
|
||||||
|
else:
|
||||||
|
params['quiet'] = True
|
||||||
|
|
||||||
if self.info.ytdlp_cookies:
|
if self.info.ytdlp_cookies:
|
||||||
try:
|
try:
|
||||||
data = jsonCookie(json.loads(self.info.ytdlp_cookies))
|
data = jsonCookie(json.loads(self.info.ytdlp_cookies))
|
||||||
|
|
@ -113,6 +122,8 @@ class Download:
|
||||||
logging.error(
|
logging.error(
|
||||||
f'Invalid cookies: was provided for {self.info.title} - {str(e)}')
|
f'Invalid cookies: was provided for {self.info.title} - {str(e)}')
|
||||||
|
|
||||||
|
logging.debug(
|
||||||
|
f'Downloading {self.info._id} {self.info.title}... {params}')
|
||||||
ret = yt_dlp.YoutubeDL(params=params).download([self.info.url])
|
ret = yt_dlp.YoutubeDL(params=params).download([self.info.url])
|
||||||
|
|
||||||
self.status_queue.put(
|
self.status_queue.put(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from src.Notifier import Notifier
|
||||||
from src.Download import Download
|
from src.Download import Download
|
||||||
from src.DTO.ItemDTO import ItemDTO
|
from src.DTO.ItemDTO import ItemDTO
|
||||||
from src.DataStore import DataStore
|
from src.DataStore import DataStore
|
||||||
from src.Utils import ObjectSerializer, calcDownloadPath, ExtractInfo
|
from src.Utils import ObjectSerializer, calcDownloadPath, ExtractInfo, mergeConfig
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
log = logging.getLogger('DownloadQueue')
|
log = logging.getLogger('DownloadQueue')
|
||||||
|
|
@ -129,6 +129,7 @@ class DownloadQueue:
|
||||||
temp_dir=self.config.temp_path,
|
temp_dir=self.config.temp_path,
|
||||||
output_template_chapter=output_chapter,
|
output_template_chapter=output_chapter,
|
||||||
default_ytdl_opts=self.config.ytdl_options,
|
default_ytdl_opts=self.config.ytdl_options,
|
||||||
|
debug=bool(self.config.ytdl_debug)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -163,6 +164,8 @@ class DownloadQueue:
|
||||||
output_template: str = '',
|
output_template: str = '',
|
||||||
already=None
|
already=None
|
||||||
):
|
):
|
||||||
|
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f'adding {url}: {quality=} {format=} {folder=} {output_template=} {ytdlp_cookies=} {ytdlp_config=}')
|
f'adding {url}: {quality=} {format=} {folder=} {output_template=} {ytdlp_cookies=} {ytdlp_config=}')
|
||||||
|
|
||||||
|
|
@ -173,7 +176,19 @@ class DownloadQueue:
|
||||||
else:
|
else:
|
||||||
already.add(url)
|
already.add(url)
|
||||||
try:
|
try:
|
||||||
entry = await asyncio.get_running_loop().run_in_executor(None, ExtractInfo, self.config.ytdl_options, url)
|
entry = await asyncio.get_running_loop().run_in_executor(
|
||||||
|
None,
|
||||||
|
ExtractInfo,
|
||||||
|
mergeConfig(self.config.ytdl_options, ytdlp_config),
|
||||||
|
url,
|
||||||
|
bool(self.config.ytdl_debug)
|
||||||
|
)
|
||||||
|
if not entry:
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'msg': 'No metadata, most likely video has been downloaded before.' if self.config.keep_archive else 'Unable to extract info check logs.'
|
||||||
|
}
|
||||||
|
logging.debug(f'entry: extract info says: {entry}')
|
||||||
except yt_dlp.utils.YoutubeDLError as exc:
|
except yt_dlp.utils.YoutubeDLError as exc:
|
||||||
return {'status': 'error', 'msg': str(exc)}
|
return {'status': 'error', 'msg': str(exc)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,9 +165,8 @@ def calcDownloadPath(basePath: str, folder: str = None) -> str:
|
||||||
return download_path
|
return download_path
|
||||||
|
|
||||||
|
|
||||||
def ExtractInfo(config: dict, url: str) -> dict:
|
def ExtractInfo(config: dict, url: str, debug: bool = False) -> dict:
|
||||||
params: dict = {
|
params: dict = {
|
||||||
'quiet': True,
|
|
||||||
'no_color': True,
|
'no_color': True,
|
||||||
'extract_flat': True,
|
'extract_flat': True,
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
|
@ -176,6 +175,12 @@ def ExtractInfo(config: dict, url: str) -> dict:
|
||||||
**config,
|
**config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
params['verbose'] = True
|
||||||
|
params['logger'] = logging.getLogger('YTPTube-ytdl')
|
||||||
|
else:
|
||||||
|
params['quiet'] = True
|
||||||
|
|
||||||
return yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
|
return yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
|
|
@ -1,16 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "YTPTube",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "YTPTube",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "^10.6.1",
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
|
|
@ -8021,6 +8022,14 @@
|
||||||
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.29.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "^10.6.1",
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
|
|
|
||||||
|
|
@ -156,12 +156,13 @@ const selectedQuality = useStorage('selectedQuality', '')
|
||||||
const ytdlpConfig = useStorage('ytdlp_config', '')
|
const ytdlpConfig = useStorage('ytdlp_config', '')
|
||||||
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
const ytdlpCookies = useStorage('ytdlp_cookies', '')
|
||||||
const output_template = useStorage('output_template', null)
|
const output_template = useStorage('output_template', null)
|
||||||
const qualities = ref([])
|
const downloadPath = useStorage('downloadPath', null)
|
||||||
const url = ref('')
|
const url = useStorage('downloadUrl', null)
|
||||||
const downloadPath = ref('')
|
|
||||||
const addInProgress = ref(false)
|
|
||||||
const showAdvanced = useStorage('show_advanced', false)
|
const showAdvanced = useStorage('show_advanced', false)
|
||||||
|
|
||||||
|
const qualities = ref([])
|
||||||
|
const addInProgress = ref(false)
|
||||||
|
|
||||||
const updateQualities = () => {
|
const updateQualities = () => {
|
||||||
for (const key in downloadFormats) {
|
for (const key in downloadFormats) {
|
||||||
const item = downloadFormats[key];
|
const item = downloadFormats[key];
|
||||||
|
|
@ -190,7 +191,7 @@ const addDownload = () => {
|
||||||
url: url.value,
|
url: url.value,
|
||||||
format: selectedFormat.value,
|
format: selectedFormat.value,
|
||||||
quality: selectedQuality.value,
|
quality: selectedQuality.value,
|
||||||
path: downloadPath.value,
|
folder: downloadPath.value,
|
||||||
ytdlp_config: ytdlpConfig.value,
|
ytdlp_config: ytdlpConfig.value,
|
||||||
ytdlp_cookies: ytdlpCookies.value,
|
ytdlp_cookies: ytdlpCookies.value,
|
||||||
output_template: output_template.value,
|
output_template: output_template.value,
|
||||||
|
|
@ -206,6 +207,8 @@ const resetStorage = () => {
|
||||||
ytdlpConfig.value = '';
|
ytdlpConfig.value = '';
|
||||||
ytdlpCookies.value = '';
|
ytdlpCookies.value = '';
|
||||||
output_template.value = null;
|
output_template.value = null;
|
||||||
|
url.value = '';
|
||||||
|
downloadPath.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.on((event, data) => {
|
bus.on((event, data) => {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
<div class="column is-12" v-if="item.error">
|
<div class="column is-12" v-if="item.error">
|
||||||
<span class="has-text-danger">{{ item.error }}</span>
|
<span class="has-text-danger">{{ item.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-4 has-text-centered">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i v-if="item.status == 'finished'" class="has-text-success fa-solid fa-circle-check"></i>
|
<i v-if="item.status == 'finished'" class="has-text-success fa-solid fa-circle-check"></i>
|
||||||
|
|
@ -83,7 +83,12 @@
|
||||||
<span>{{ capitalize(item.status) }}</span>
|
<span>{{ capitalize(item.status) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-4 has-text-centered">
|
||||||
|
<span :data-tooltip="moment(item.timestamp / 1000000).format('MMMM Do YYYY, h:mm:ss a')">
|
||||||
|
{{ moment(item.timestamp / 1000000).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4 has-text-centered">
|
||||||
<label class="checkbox is-block">
|
<label class="checkbox is-block">
|
||||||
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
|
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
|
||||||
:value="item._id">
|
:value="item._id">
|
||||||
|
|
@ -159,6 +164,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed, ref, watch, defineEmits } from 'vue';
|
import { defineProps, computed, ref, watch, defineEmits } from 'vue';
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
const emits = defineEmits(['deleteItem', 'addItem']);
|
const emits = defineEmits(['deleteItem', 'addItem']);
|
||||||
|
|
||||||
|
|
@ -287,7 +293,7 @@ const reQueueItem = (id, item) => {
|
||||||
url: item.url,
|
url: item.url,
|
||||||
format: item.format,
|
format: item.format,
|
||||||
quality: item.quality,
|
quality: item.quality,
|
||||||
path: item.folder,
|
folder: item.folder,
|
||||||
ytdlp_config: item.ytdlp_config,
|
ytdlp_config: item.ytdlp_config,
|
||||||
ytdlp_cookies: item.ytdlp_cookies,
|
ytdlp_cookies: item.ytdlp_cookies,
|
||||||
output_template: item.output_template,
|
output_template: item.output_template,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
:value="item.percent ? percentPipe(item.percent) : ''" max="100">
|
:value="item.percent ? percentPipe(item.percent) : ''" max="100">
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-4 has-text-centered">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i v-if="item.status == 'finished'" class="has-text-success fa-solid fa-circle-check"></i>
|
<i v-if="item.status == 'finished'" class="has-text-success fa-solid fa-circle-check"></i>
|
||||||
|
|
@ -52,7 +52,12 @@
|
||||||
<span>{{ capitalize(item.status) }}</span>
|
<span>{{ capitalize(item.status) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-4 has-text-centered">
|
||||||
|
<span :data-tooltip="moment(item.timestamp / 1000000).format('MMMM Do YYYY, h:mm:ss a')">
|
||||||
|
{{ moment(item.timestamp / 1000000).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4 has-text-centered">
|
||||||
<label class="checkbox is-block">
|
<label class="checkbox is-block">
|
||||||
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :value="item._id">
|
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :value="item._id">
|
||||||
Select
|
Select
|
||||||
|
|
@ -61,8 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<a class="button is-danger is-fullwidth"
|
<a class="button is-danger is-fullwidth" @click="$emit('deleteItem', 'queue', item._id)">
|
||||||
@click="$emit('deleteItem', 'queue', item._id)">
|
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa-solid fa-trash-can"></i>
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
|
|
@ -107,6 +111,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, watch, computed } from 'vue';
|
import { defineProps, defineEmits, ref, watch, computed } from 'vue';
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
defineEmits(['deleteItem']);
|
defineEmits(['deleteItem']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ app.config.globalProperties.makeDownload = (config, item) => {
|
||||||
|
|
||||||
app.use(Toast, {
|
app.use(Toast, {
|
||||||
transition: "Vue-Toastification__bounce",
|
transition: "Vue-Toastification__bounce",
|
||||||
|
position: "bottom-right",
|
||||||
maxToasts: 5,
|
maxToasts: 5,
|
||||||
newestOnTop: true
|
newestOnTop: true
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue