Merge pull request #262 from arabcoders/dev
Automatically re-queue upcoming live streams in history.
This commit is contained in:
commit
b29dc00bd8
7 changed files with 176 additions and 45 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -48,10 +48,13 @@
|
||||||
"preferredquality",
|
"preferredquality",
|
||||||
"printtraffic",
|
"printtraffic",
|
||||||
"quicktime",
|
"quicktime",
|
||||||
|
"rtime",
|
||||||
|
"smhd",
|
||||||
"timespec",
|
"timespec",
|
||||||
"tmpfilename",
|
"tmpfilename",
|
||||||
"upgrader",
|
"upgrader",
|
||||||
"urandom",
|
"urandom",
|
||||||
|
"usegmt",
|
||||||
"vcodec",
|
"vcodec",
|
||||||
"vconvert",
|
"vconvert",
|
||||||
"writedescription",
|
"writedescription",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
||||||
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
||||||
* Support for both advanced and basic mode for WebUI.
|
* Support for both advanced and basic mode for WebUI.
|
||||||
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
||||||
|
* Automatic upcoming live stream re-queue.
|
||||||
|
|
||||||
# Run using docker command
|
# Run using docker command
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ from .ItemDTO import Item, ItemDTO
|
||||||
from .Presets import Presets
|
from .Presets import Presets
|
||||||
from .Scheduler import Scheduler
|
from .Scheduler import Scheduler
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
from .Utils import arg_converter, calc_download_path, extract_info, is_downloaded, load_cookies
|
from .Utils import arg_converter, calc_download_path, dt_delta, extract_info, is_downloaded, load_cookies
|
||||||
from .YTDLPOpts import YTDLPOpts
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("DownloadQueue")
|
LOG = logging.getLogger("DownloadQueue")
|
||||||
|
|
@ -95,9 +95,14 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
Scheduler.get_instance().add(
|
Scheduler.get_instance().add(
|
||||||
timer="* * * * *",
|
timer="* * * * *",
|
||||||
func=self.monitor_queue_for_stale_items,
|
func=self.monitor_queue_for_stale_items,
|
||||||
id=f"{__class__.__name__}.monitor_queue_for_stale_items",
|
id=f"{__class__.__name__}.{__class__.monitor_queue_for_stale_items.__name__}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Scheduler.get_instance().add(
|
||||||
|
timer="* * * * *",
|
||||||
|
func=self.monitor_queue_live,
|
||||||
|
id=f"{__class__.__name__}.{__class__.monitor_queue_live.__name__}",
|
||||||
|
)
|
||||||
# app.on_shutdown.append(self.on_shutdown)
|
# app.on_shutdown.append(self.on_shutdown)
|
||||||
|
|
||||||
# async def close_pool(_: web.Application):
|
# async def close_pool(_: web.Application):
|
||||||
|
|
@ -195,6 +200,8 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
for key in item.extras.copy():
|
for key in item.extras.copy():
|
||||||
if not self.keep_extra_key(key):
|
if not self.keep_extra_key(key):
|
||||||
item.extras.pop(key)
|
item.extras.pop(key)
|
||||||
|
else:
|
||||||
|
item.extras = {}
|
||||||
|
|
||||||
if not entry:
|
if not entry:
|
||||||
return {"status": "error", "msg": "Invalid/empty data was given."}
|
return {"status": "error", "msg": "Invalid/empty data was given."}
|
||||||
|
|
@ -243,8 +250,9 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
if ("video" == eventType or eventType.startswith("url")) and "id" in entry and "title" in entry:
|
if ("video" == eventType or eventType.startswith("url")) and "id" in entry and "title" in entry:
|
||||||
# check if the video is live stream.
|
# check if the video is live stream.
|
||||||
if "live_status" in entry and "is_upcoming" == entry.get("live_status"):
|
if "live_status" in entry and "is_upcoming" == entry.get("live_status"):
|
||||||
if "release_timestamp" in entry and entry.get("release_timestamp"):
|
if entry.get("release_timestamp"):
|
||||||
live_in = formatdate(entry.get("release_timestamp"))
|
live_in = formatdate(entry.get("release_timestamp"), usegmt=True)
|
||||||
|
item.extras.update({"live_in": live_in})
|
||||||
else:
|
else:
|
||||||
error = "Live stream not yet started. And no date is set."
|
error = "Live stream not yet started. And no date is set."
|
||||||
else:
|
else:
|
||||||
|
|
@ -304,7 +312,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
datetime=formatdate(time.time()),
|
datetime=formatdate(time.time()),
|
||||||
error=error,
|
error=error,
|
||||||
is_live=is_live,
|
is_live=is_live,
|
||||||
live_in=live_in,
|
live_in=live_in if live_in else item.extras.get("live_in", None),
|
||||||
options=options,
|
options=options,
|
||||||
cli=item.cli,
|
cli=item.cli,
|
||||||
extras=item.extras,
|
extras=item.extras,
|
||||||
|
|
@ -313,7 +321,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
try:
|
try:
|
||||||
dlInfo: Download = Download(info=dl, info_dict=entry)
|
dlInfo: Download = Download(info=dl, info_dict=entry)
|
||||||
|
|
||||||
if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"):
|
if live_in or "is_upcoming" == entry.get("live_status"):
|
||||||
NotifyEvent = Events.COMPLETED
|
NotifyEvent = Events.COMPLETED
|
||||||
dlInfo.info.status = "not_live"
|
dlInfo.info.status = "not_live"
|
||||||
dlInfo.info.msg = "Stream is not live yet."
|
dlInfo.info.msg = "Stream is not live yet."
|
||||||
|
|
@ -745,14 +753,14 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
bool: True if the extra key should be kept, False otherwise.
|
bool: True if the extra key should be kept, False otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
keys = ("playlist", "external_downloader")
|
keys = ("playlist", "external_downloader", "live_in")
|
||||||
return any(key == k or key.startswith(k) for k in keys)
|
return any(key == k or key.startswith(k) for k in keys)
|
||||||
|
|
||||||
async def monitor_queue_for_stale_items(self):
|
async def monitor_queue_for_stale_items(self):
|
||||||
"""
|
"""
|
||||||
Monitor the queue for stale items and cancel them if needed.
|
Monitor the queue for stale items and cancel them if needed.
|
||||||
"""
|
"""
|
||||||
if self.is_paused() or not self.queue.has_downloads():
|
if self.is_paused() or self.queue.empty():
|
||||||
return
|
return
|
||||||
|
|
||||||
LOG.debug("Checking for stale items in the download queue.")
|
LOG.debug("Checking for stale items in the download queue.")
|
||||||
|
|
@ -767,3 +775,60 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
await self.cancel([id])
|
await self.cancel([id])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to cancel staled item '{item_ref}'. {e!s}")
|
LOG.error(f"Failed to cancel staled item '{item_ref}'. {e!s}")
|
||||||
|
LOG.exception(e)
|
||||||
|
|
||||||
|
async def monitor_queue_live(self):
|
||||||
|
"""
|
||||||
|
Monitor the queue for items marked as live events and queue them when time is reached.
|
||||||
|
"""
|
||||||
|
if self.is_paused() or self.done.empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.debug("Checking for live stream items in the history queue.")
|
||||||
|
|
||||||
|
time_now = datetime.now(tz=UTC)
|
||||||
|
|
||||||
|
status = ["not_live", "is_upcoming", "is_live"]
|
||||||
|
|
||||||
|
for id, item in list(self.done.items()):
|
||||||
|
if item.info.status not in status:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_ref = f"{id=} {item.info.id=} {item.info.title=}"
|
||||||
|
if not item.is_live:
|
||||||
|
LOG.debug(f"Item '{item_ref}' is not a live stream.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not item.info.live_in:
|
||||||
|
LOG.debug(f"Item '{item_ref}' marked as live stream, but no date is set.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
starts_in = parsedate_to_datetime(item.info.live_in)
|
||||||
|
|
||||||
|
if time_now < (starts_in + timedelta(minutes=1)):
|
||||||
|
LOG.debug(f"Item '{item_ref}' is not yet live. will start in '{dt_delta(starts_in-time_now)}'.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
LOG.info(f"Re-queuing item '{item_ref} {item.info.extras=}' for download.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.clear([item.info._id], remove_file=False)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to clear item '{item_ref}'. {e!s}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = item.info
|
||||||
|
new_queue = Item(
|
||||||
|
url=info.url,
|
||||||
|
preset=info.preset,
|
||||||
|
folder=info.folder,
|
||||||
|
cookies=info.cookies,
|
||||||
|
template=info.template,
|
||||||
|
cli=item.info.cli,
|
||||||
|
extras=item.info.extras,
|
||||||
|
)
|
||||||
|
await self.add(item=new_queue)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to re-queue item '{item_ref}'. {e!s}")
|
||||||
|
LOG.exception(e)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
|
||||||
import glob
|
import glob
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
|
|
@ -11,6 +10,7 @@ import re
|
||||||
import shlex
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from http.cookiejar import MozillaCookieJar
|
from http.cookiejar import MozillaCookieJar
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -68,7 +68,7 @@ class StreamingError(Exception):
|
||||||
|
|
||||||
class FileLogFormatter(logging.Formatter):
|
class FileLogFormatter(logging.Formatter):
|
||||||
def formatTime(self, record, datefmt=None): # noqa: ARG002, N802
|
def formatTime(self, record, datefmt=None): # noqa: ARG002, N802
|
||||||
return datetime.datetime.fromtimestamp(record.created).astimezone().isoformat(timespec="milliseconds")
|
return datetime.fromtimestamp(record.created).astimezone().isoformat(timespec="milliseconds")
|
||||||
|
|
||||||
|
|
||||||
def calc_download_path(base_path: str, folder: str | None = None, create_path: bool = True) -> str:
|
def calc_download_path(base_path: str, folder: str | None = None, create_path: bool = True) -> str:
|
||||||
|
|
@ -844,8 +844,8 @@ def get_files(base_path: str, dir: str | None = None):
|
||||||
"path": str(file).replace(base_path, "").strip("/"),
|
"path": str(file).replace(base_path, "").strip("/"),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"mime": get_mime_type({}, file) if file.is_file() else "directory",
|
"mime": get_mime_type({}, file) if file.is_file() else "directory",
|
||||||
"mtime": datetime.datetime.fromtimestamp(stat.st_mtime, tz=datetime.UTC).isoformat(),
|
"mtime": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
|
||||||
"ctime": datetime.datetime.fromtimestamp(stat.st_ctime, tz=datetime.UTC).isoformat(),
|
"ctime": datetime.fromtimestamp(stat.st_ctime, tz=UTC).isoformat(),
|
||||||
"is_dir": file.is_dir(),
|
"is_dir": file.is_dir(),
|
||||||
"is_file": file.is_file(),
|
"is_file": file.is_file(),
|
||||||
}
|
}
|
||||||
|
|
@ -1089,3 +1089,36 @@ def get_archive_id(url: str) -> tuple[bool, dict[str | None, str | None, str | N
|
||||||
break
|
break
|
||||||
|
|
||||||
return idDict
|
return idDict
|
||||||
|
|
||||||
|
|
||||||
|
def dt_delta(delta: timedelta) -> str:
|
||||||
|
"""
|
||||||
|
Convert a timedelta object to a human-readable string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delta (timedelta): The timedelta object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A human-readable string representing the timedelta.
|
||||||
|
|
||||||
|
"""
|
||||||
|
total_secs = int(delta.total_seconds())
|
||||||
|
days, rem = divmod(total_secs, 86400)
|
||||||
|
hours, rem = divmod(rem, 3600)
|
||||||
|
minutes, secs = divmod(rem, 60)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if days:
|
||||||
|
parts.append(f"{days}d")
|
||||||
|
if hours:
|
||||||
|
parts.append(f"{hours}h")
|
||||||
|
if minutes:
|
||||||
|
parts.append(f"{minutes}m")
|
||||||
|
if secs:
|
||||||
|
parts.append(f"{secs}s")
|
||||||
|
|
||||||
|
# if it's under one second
|
||||||
|
if not parts:
|
||||||
|
parts.append("<1s")
|
||||||
|
|
||||||
|
return " ".join(parts)
|
||||||
|
|
|
||||||
|
|
@ -125,16 +125,13 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered has-text-centered is-unselectable">
|
<td class="is-vcentered has-text-centered is-unselectable">
|
||||||
<span class="user-hint" :date-datetime="item.datetime"
|
<span class="user-hint" :date-datetime="item.datetime"
|
||||||
v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')" v-rtime="item.datetime" />
|
||||||
{{ moment(item.datetime).fromNow() }}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered has-text-centered is-unselectable"
|
<td class="is-vcentered has-text-centered is-unselectable"
|
||||||
v-if="item.live_in && 'not_live' === item.status">
|
v-if="item.live_in && 'not_live' === item.status">
|
||||||
<span :date-datetime="item.live_in" class="user-hint"
|
<span :date-datetime="item.live_in" class="user-hint"
|
||||||
v-tooltip="'Starts at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
|
v-tooltip="'Will automatically be requeued at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')"
|
||||||
{{ moment(item.live_in).fromNow() }}
|
v-rtime="item.live_in" />
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered has-text-centered is-unselectable" v-else>
|
<td class="is-vcentered has-text-centered is-unselectable" v-else>
|
||||||
{{ item.file_size ? formatBytes(item.file_size) : '-' }}
|
{{ item.file_size ? formatBytes(item.file_size) : '-' }}
|
||||||
|
|
@ -259,15 +256,12 @@
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
|
||||||
v-if="item.live_in && 'not_live' === item.status">
|
v-if="item.live_in && 'not_live' === item.status">
|
||||||
<span :date-datetime="item.live_in" class="user-hint"
|
<span :date-datetime="item.live_in" class="user-hint"
|
||||||
v-tooltip="'Starts at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
|
v-tooltip="'Will automatically be requeued at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')"
|
||||||
{{ moment(item.live_in).fromNow() }}
|
v-rtime="item.live_in" />
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
||||||
<span class="user-hint" :date-datetime="item.datetime"
|
<span class="user-hint" :date-datetime="item.datetime"
|
||||||
v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')" v-rtime="item.datetime" />
|
||||||
{{ moment(item.datetime).fromNow() }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
|
||||||
v-if="item.file_size">
|
v-if="item.file_size">
|
||||||
|
|
@ -624,16 +618,6 @@ const removeItem = item => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reQueueItem = (item, event = null) => {
|
const reQueueItem = (item, event = null) => {
|
||||||
let extras = {}
|
|
||||||
|
|
||||||
if (item?.extras) {
|
|
||||||
Object.keys(item.extras).forEach(k => {
|
|
||||||
if (k && true === k.startsWith('playlist')) {
|
|
||||||
extras[k] = item.extras[k]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const item_req = {
|
const item_req = {
|
||||||
url: item.url,
|
url: item.url,
|
||||||
preset: item.preset,
|
preset: item.preset,
|
||||||
|
|
@ -641,7 +625,7 @@ const reQueueItem = (item, event = null) => {
|
||||||
cookies: item.cookies,
|
cookies: item.cookies,
|
||||||
template: item.template,
|
template: item.template,
|
||||||
cli: item?.cli,
|
cli: item?.cli,
|
||||||
extras: extras
|
extras: item?.extras && Object.keys(item.extras) > 0 ? item.extras : {},
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.emit('item_delete', { id: item._id, remove_file: false })
|
socket.emit('item_delete', { id: item._id, remove_file: false })
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,8 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-centered is-text-overflow is-unselectable">
|
<td class="has-text-centered is-text-overflow is-unselectable">
|
||||||
<span :data-datetime="item.datetime"
|
<span v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')"
|
||||||
v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
|
:data-datetime="item.datetime" v-rtime="item.datetime" />
|
||||||
{{ moment(item.datetime).fromNow() }}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered is-items-center">
|
<td class="is-vcentered is-items-center">
|
||||||
<div class="field is-grouped is-grouped-centered">
|
<div class="field is-grouped is-grouped-centered">
|
||||||
|
|
@ -167,10 +165,8 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
||||||
<span :data-datetime="item.datetime"
|
<span v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')" :data-datetime="item.datetime"
|
||||||
v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
|
v-rtime="item.datetime" />
|
||||||
{{ moment(item.datetime).fromNow() }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
||||||
<label class="checkbox is-block">
|
<label class="checkbox is-block">
|
||||||
|
|
|
||||||
49
ui/plugins/rtime.js
Normal file
49
ui/plugins/rtime.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
const parseInterval = arg => {
|
||||||
|
const match = arg.match(/^(\d+)([smhd])$/)
|
||||||
|
if (!match) {
|
||||||
|
return 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, numStr, unit] = match
|
||||||
|
const num = parseInt(numStr, 10)
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'd': return num * 24 * 3600 * 1000
|
||||||
|
case 'h': return num * 3600 * 1000
|
||||||
|
case 'm': return num * 60 * 1000
|
||||||
|
case 's': return num * 1000
|
||||||
|
default: return 60 * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(nuxtApp => nuxtApp.vueApp.directive('rtime', {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const intervalMs = binding.arg ? parseInterval(binding.arg) : 60 * 1000
|
||||||
|
const update = () => el.textContent = moment(binding.value).fromNow()
|
||||||
|
|
||||||
|
update()
|
||||||
|
el._next_timer = window.setInterval(update, intervalMs)
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
if (binding.oldValue !== binding.value) {
|
||||||
|
if (null != el._next_timer) {
|
||||||
|
clearInterval(el._next_timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalMs = binding.arg ? parseInterval(binding.arg) : 60 * 1000
|
||||||
|
|
||||||
|
const update = () => el.textContent = moment(binding.value).fromNow()
|
||||||
|
update()
|
||||||
|
|
||||||
|
el._next_timer = window.setInterval(update, intervalMs)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount(el) {
|
||||||
|
if (null != el._next_timer) {
|
||||||
|
clearInterval(el._next_timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
Loading…
Reference in a new issue