Compare commits
10
Commits
edb533f3f8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c092140d2f | ||
|
|
51b63eb6b6 | ||
|
|
f1d29d5109 | ||
|
|
d2d8c7dbf6 | ||
|
|
46eabcecd6 | ||
|
|
ad2c20ea39 | ||
|
|
ddd97c12aa | ||
|
|
7b7b7512f5 | ||
|
|
a8b1d41398 | ||
|
|
2dc08013b7 |
@@ -2,11 +2,14 @@
|
|||||||
"""Normalize photo and image filenames from phones and other sources."""
|
"""Normalize photo and image filenames from phones and other sources."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PHONE_PATTERN = re.compile(
|
PHONE_PATTERN = re.compile(
|
||||||
@@ -24,27 +27,127 @@ TELEGRAM_PATTERN = re.compile(
|
|||||||
|
|
||||||
NORMALIZED = re.compile(r'^\d{4}-\d{2}-\d{2} .+')
|
NORMALIZED = re.compile(r'^\d{4}-\d{2}-\d{2} .+')
|
||||||
|
|
||||||
|
# Filename prefixes whose embedded timestamp is UTC rather than local time.
|
||||||
|
# The Pixel camera names captures in UTC but records the local wall clock time
|
||||||
|
# in EXIF, so for these the metadata is authoritative and the filename is not.
|
||||||
|
# Other sources (Samsung's IMG_/VID_, Telegram exports) already name in local
|
||||||
|
# time and are left alone.
|
||||||
|
UTC_FILENAME_PREFIXES = ('PXL',)
|
||||||
|
|
||||||
|
# e.g. "PXL_20260917_163001280.MP" -> prefix, timestamp, subseconds, suffix
|
||||||
|
UTC_STEM_PATTERN = re.compile(
|
||||||
|
r'^(' + '|'.join(UTC_FILENAME_PREFIXES) + r')[_-]'
|
||||||
|
r'(\d{8}[_-]\d{6})'
|
||||||
|
r'(\d*)'
|
||||||
|
r'(.*)$',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Windows hidden-file attribute, mirrored from the stat module.
|
||||||
|
FILE_ATTRIBUTE_HIDDEN = 0x2
|
||||||
|
|
||||||
STATUS_RENAME = 'rename'
|
STATUS_RENAME = 'rename'
|
||||||
STATUS_ALREADY = 'skip (already normalized)'
|
STATUS_ALREADY = 'skip (already normalized)'
|
||||||
STATUS_NO_MATCH = 'skip (no rule matched)'
|
STATUS_NO_MATCH = 'skip (no rule matched)'
|
||||||
STATUS_CONFLICT = 'skip (conflict)'
|
STATUS_CONFLICT = 'skip (conflict)'
|
||||||
|
|
||||||
|
|
||||||
def get_exif_date(filepath):
|
@lru_cache(maxsize=1)
|
||||||
"""Try to get the image creation date via exiftool."""
|
def has_exiftool():
|
||||||
|
"""Check once whether exiftool is available on PATH."""
|
||||||
|
return shutil.which('exiftool') is not None
|
||||||
|
|
||||||
|
|
||||||
|
# Capture timestamps keyed by exif_key(path), filled by prefetch_exif().
|
||||||
|
_exif_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def exif_key(filepath):
|
||||||
|
"""Normalize a path so exiftool's SourceFile and our Path agree."""
|
||||||
|
return os.path.normcase(os.path.abspath(filepath))
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_exif(files):
|
||||||
|
"""Read capture timestamps for all files with a single exiftool call.
|
||||||
|
|
||||||
|
Starting exiftool dominates the cost of reading a few tags, so reading
|
||||||
|
every file in one process is far faster than one process per file.
|
||||||
|
Paths are passed on stdin to stay clear of command line length limits.
|
||||||
|
"""
|
||||||
|
files = [f for f in files if exif_key(f) not in _exif_cache]
|
||||||
|
if not files or not has_exiftool():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Files without a usable timestamp stay None so they aren't re-queried.
|
||||||
|
_exif_cache.update((exif_key(f), None) for f in files)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'-json', '-fast', '-charset', 'filename=utf8',
|
||||||
|
'-d', '%Y%m%d_%H%M%S',
|
||||||
|
'-DateTimeOriginal', '-SubSecTimeOriginal',
|
||||||
|
*(str(f) for f in files),
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['exiftool', '-s3', '-d', '%Y-%m-%d', '-DateTimeOriginal', str(filepath)],
|
['exiftool', '-@', '-'],
|
||||||
|
input='\n'.join(args),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
encoding='utf-8',
|
||||||
timeout=10,
|
timeout=60 + len(files),
|
||||||
)
|
)
|
||||||
date_str = result.stdout.strip()
|
records = json.loads(result.stdout or '[]')
|
||||||
if date_str and re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
||||||
return date_str
|
return
|
||||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
||||||
pass
|
for record in records:
|
||||||
|
stamp = str(record.get('DateTimeOriginal', ''))
|
||||||
|
if not re.match(r'^\d{8}_\d{6}$', stamp):
|
||||||
|
continue
|
||||||
|
subsec = str(record.get('SubSecTimeOriginal', ''))
|
||||||
|
_exif_cache[exif_key(record['SourceFile'])] = (
|
||||||
|
stamp, subsec if subsec.isdigit() else '',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_exif_date(filepath):
|
||||||
|
"""Get the image creation date (YYYY-MM-DD) from EXIF, or None."""
|
||||||
|
stamp = get_exif_timestamp(filepath)
|
||||||
|
if stamp is None:
|
||||||
return None
|
return None
|
||||||
|
day = stamp[0]
|
||||||
|
return f'{day[:4]}-{day[4:6]}-{day[6:8]}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_exif_timestamp(filepath):
|
||||||
|
"""Get the local capture time from EXIF as (YYYYMMDD_HHMMSS, subsec).
|
||||||
|
|
||||||
|
DateTimeOriginal is the wall clock time at the moment of capture, so it
|
||||||
|
needs no timezone correction. Returns None when unavailable.
|
||||||
|
"""
|
||||||
|
prefetch_exif([filepath])
|
||||||
|
return _exif_cache.get(exif_key(filepath))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_stem(filepath):
|
||||||
|
"""Return the file's stem, with UTC-named timestamps corrected from EXIF.
|
||||||
|
|
||||||
|
Pixel filenames encode UTC, so the name alone is off by the UTC offset in
|
||||||
|
effect at capture. Falls back to the original stem when the file is not a
|
||||||
|
UTC-named capture, or when its metadata has no usable timestamp.
|
||||||
|
"""
|
||||||
|
stem = filepath.stem
|
||||||
|
|
||||||
|
match = UTC_STEM_PATTERN.match(stem)
|
||||||
|
if not match or not has_exiftool():
|
||||||
|
return stem
|
||||||
|
|
||||||
|
prefix, _, subsec, rest = match.groups()
|
||||||
|
stamp = get_exif_timestamp(filepath)
|
||||||
|
if stamp is None:
|
||||||
|
return stem
|
||||||
|
|
||||||
|
exif_stamp, exif_subsec = stamp
|
||||||
|
return f'{prefix}_{exif_stamp}{exif_subsec or subsec}{rest}'
|
||||||
|
|
||||||
|
|
||||||
def get_file_date(filepath):
|
def get_file_date(filepath):
|
||||||
@@ -56,12 +159,12 @@ def get_file_date(filepath):
|
|||||||
|
|
||||||
def plan_default_rename(filepath):
|
def plan_default_rename(filepath):
|
||||||
"""Plan a rename using the default pattern-matching mode."""
|
"""Plan a rename using the default pattern-matching mode."""
|
||||||
stem = filepath.stem
|
|
||||||
ext = filepath.suffix
|
|
||||||
|
|
||||||
if NORMALIZED.match(filepath.name):
|
if NORMALIZED.match(filepath.name):
|
||||||
return None, STATUS_ALREADY
|
return None, STATUS_ALREADY
|
||||||
|
|
||||||
|
stem = resolve_stem(filepath)
|
||||||
|
ext = filepath.suffix
|
||||||
|
|
||||||
tg_match = TELEGRAM_PATTERN.match(stem)
|
tg_match = TELEGRAM_PATTERN.match(stem)
|
||||||
if tg_match:
|
if tg_match:
|
||||||
number, day, month, year, hour, minute, second = tg_match.groups()
|
number, day, month, year, hour, minute, second = tg_match.groups()
|
||||||
@@ -69,7 +172,7 @@ def plan_default_rename(filepath):
|
|||||||
|
|
||||||
match = PHONE_PATTERN.match(stem)
|
match = PHONE_PATTERN.match(stem)
|
||||||
if not match:
|
if not match:
|
||||||
match = PHONE_PATTERN.match(filepath.name)
|
match = PHONE_PATTERN.match(f'{stem}{ext}')
|
||||||
if match:
|
if match:
|
||||||
year, month, day, rest = match.groups()
|
year, month, day, rest = match.groups()
|
||||||
new_name = f'{year}-{month}-{day} {rest}'
|
new_name = f'{year}-{month}-{day} {rest}'
|
||||||
@@ -88,7 +191,7 @@ def plan_parse_rename(filepath):
|
|||||||
return None, STATUS_ALREADY
|
return None, STATUS_ALREADY
|
||||||
|
|
||||||
date_str = None
|
date_str = None
|
||||||
if shutil.which('exiftool') is not None:
|
if has_exiftool():
|
||||||
date_str = get_exif_date(filepath)
|
date_str = get_exif_date(filepath)
|
||||||
|
|
||||||
if not date_str:
|
if not date_str:
|
||||||
@@ -115,14 +218,14 @@ def get_date_for_file(filepath, parse_mode):
|
|||||||
"""Resolve a date string for a file, or None if unavailable."""
|
"""Resolve a date string for a file, or None if unavailable."""
|
||||||
if parse_mode:
|
if parse_mode:
|
||||||
date_str = None
|
date_str = None
|
||||||
if shutil.which('exiftool') is not None:
|
if has_exiftool():
|
||||||
date_str = get_exif_date(filepath)
|
date_str = get_exif_date(filepath)
|
||||||
if not date_str:
|
if not date_str:
|
||||||
date_str = get_file_date(filepath)
|
date_str = get_file_date(filepath)
|
||||||
return date_str
|
return date_str
|
||||||
|
|
||||||
# Default pattern mode: extract date from filename
|
# Default pattern mode: extract date from filename
|
||||||
stem = filepath.stem
|
stem = resolve_stem(filepath)
|
||||||
|
|
||||||
# Check if already normalized (e.g. "2024-06-01 breakfast.jpg")
|
# Check if already normalized (e.g. "2024-06-01 breakfast.jpg")
|
||||||
norm_match = re.match(r'^(\d{4})-(\d{2})-(\d{2}) ', filepath.name)
|
norm_match = re.match(r'^(\d{4})-(\d{2})-(\d{2}) ', filepath.name)
|
||||||
@@ -138,7 +241,7 @@ def get_date_for_file(filepath, parse_mode):
|
|||||||
# Try the standard pattern
|
# Try the standard pattern
|
||||||
match = PHONE_PATTERN.match(stem)
|
match = PHONE_PATTERN.match(stem)
|
||||||
if not match:
|
if not match:
|
||||||
match = PHONE_PATTERN.match(filepath.name)
|
match = PHONE_PATTERN.match(f'{stem}{filepath.suffix}')
|
||||||
if match:
|
if match:
|
||||||
year, month, day, _ = match.groups()
|
year, month, day, _ = match.groups()
|
||||||
return f'{year}-{month}-{day}'
|
return f'{year}-{month}-{day}'
|
||||||
@@ -146,7 +249,19 @@ def get_date_for_file(filepath, parse_mode):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def collect_files(paths):
|
def is_hidden(filepath):
|
||||||
|
"""Check whether a file is hidden (dotfile, or hidden attribute on Windows)."""
|
||||||
|
if filepath.name.startswith('.'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
attrs = filepath.stat().st_file_attributes
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
return False
|
||||||
|
return bool(attrs & FILE_ATTRIBUTE_HIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_files(paths, include_hidden=False):
|
||||||
"""Resolve the list of files to process."""
|
"""Resolve the list of files to process."""
|
||||||
if not paths:
|
if not paths:
|
||||||
paths = ['.']
|
paths = ['.']
|
||||||
@@ -155,8 +270,12 @@ def collect_files(paths):
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
candidate = Path(path)
|
candidate = Path(path)
|
||||||
if candidate.is_dir():
|
if candidate.is_dir():
|
||||||
files.extend(sorted(file for file in candidate.iterdir() if file.is_file()))
|
files.extend(sorted(
|
||||||
|
file for file in candidate.iterdir()
|
||||||
|
if file.is_file() and (include_hidden or not is_hidden(file))
|
||||||
|
))
|
||||||
elif candidate.is_file():
|
elif candidate.is_file():
|
||||||
|
# Explicitly named files are always processed.
|
||||||
files.append(candidate)
|
files.append(candidate)
|
||||||
return files
|
return files
|
||||||
|
|
||||||
@@ -272,13 +391,17 @@ def apply_renames(plan):
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def confirm(prompt='Apply renames? [y/n] '):
|
def confirm(prompt='Apply renames? [Y/n] '):
|
||||||
"""Ask the user for confirmation."""
|
"""Ask the user for confirmation. An empty answer accepts the default (yes)."""
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
answer = input(prompt).strip().lower()
|
answer = input(prompt).strip().lower()
|
||||||
if answer == 'y':
|
except EOFError:
|
||||||
|
print()
|
||||||
return True
|
return True
|
||||||
if answer == 'n':
|
if answer in ('', 'y', 'yes'):
|
||||||
|
return True
|
||||||
|
if answer in ('n', 'no'):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -306,6 +429,12 @@ def main():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Force numbering even for already-normalized files (used with -n).',
|
help='Force numbering even for already-normalized files (used with -n).',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a',
|
||||||
|
'--all',
|
||||||
|
action='store_true',
|
||||||
|
help='Include hidden files when scanning directories.',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'files',
|
'files',
|
||||||
nargs='*',
|
nargs='*',
|
||||||
@@ -314,11 +443,17 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
files = collect_files(args.files)
|
files = collect_files(args.files, args.all)
|
||||||
if not files:
|
if not files:
|
||||||
print('No files found.')
|
print('No files found.')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
prefetch_exif([
|
||||||
|
f for f in files
|
||||||
|
if (args.force or not NORMALIZED.match(f.name))
|
||||||
|
and (args.parse or UTC_STEM_PATTERN.match(f.stem))
|
||||||
|
])
|
||||||
|
|
||||||
if args.number:
|
if args.number:
|
||||||
plan = build_number_plan(files, args.parse, args.force)
|
plan = build_number_plan(files, args.parse, args.force)
|
||||||
else:
|
else:
|
||||||
@@ -337,4 +472,8 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
main()
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('\nAborted.')
|
||||||
|
sys.exit(1)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ case $OS in
|
|||||||
Fedora\ Linux)
|
Fedora\ Linux)
|
||||||
dnf upgrade --refresh;
|
dnf upgrade --refresh;
|
||||||
flatpak update --assumeyes;
|
flatpak update --assumeyes;
|
||||||
|
dnf needs-restarting -r;
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Cannot update system with OS: " $OS
|
echo "Cannot update system with OS: " $OS
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Inline Reddit images and Comment Redirect
|
// @name Inline Reddit images and Comment Redirect
|
||||||
// @namespace https://www.reddit.com
|
// @namespace https://www.reddit.com
|
||||||
// @version 2025-09-24
|
// @version 2026-08-28.1
|
||||||
// @description Embed images posted as comments and redirect post links to comments
|
// @description Embed images and videos posted as comments and redirect post links to comments
|
||||||
// @author luxick
|
// @author luxick
|
||||||
// @updateURL https://git.luxick.de/luxick/scripts/raw/branch/master/reddit-image-inline.user.js
|
// @updateURL https://git.luxick.de/luxick/scripts/raw/branch/master/js/reddit-image-inline.user.js
|
||||||
// @downloadURL https://git.luxick.de/luxick/scripts/raw/branch/master/reddit-image-inline.user.js
|
// @downloadURL https://git.luxick.de/luxick/scripts/raw/branch/master/js/reddit-image-inline.user.js
|
||||||
// @match https://old.reddit.com/r/*
|
// @match https://old.reddit.com/r/*
|
||||||
// @match https://www.reddit.com/r/*
|
// @match https://www.reddit.com/r/*
|
||||||
// @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
|
||||||
@@ -15,8 +15,10 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Handle image inlining
|
// Collect the placeholder links Reddit renders for media comments,
|
||||||
let xpath = "//a[text()='<image>']";
|
// e.g. "<image>" or "<video>" as the link text.
|
||||||
|
function collectLinks(placeholder) {
|
||||||
|
let xpath = "//a[text()='" + placeholder + "']";
|
||||||
var element;
|
var element;
|
||||||
var result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
|
var result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
|
||||||
let links = [];
|
let links = [];
|
||||||
@@ -25,8 +27,123 @@
|
|||||||
links.push(element);
|
links.push(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
for (var elem of links) {
|
// Comment media is linked through Reddit's viewer:
|
||||||
|
// "https://reddit.com/media?url=<media url>"
|
||||||
|
function unwrapMediaUrl(href) {
|
||||||
|
let match = href.match(/[?&]url=([^&]+)/);
|
||||||
|
if (!match) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(match[1]);
|
||||||
|
} catch (e) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video comments point at an HLS playlist, which only Safari plays without
|
||||||
|
// a library. The same upload is also served as standalone MP4 renditions,
|
||||||
|
// so use those instead.
|
||||||
|
//
|
||||||
|
// Newer uploads name them CMAF_<height>.mp4, older ones DASH_<height>.mp4,
|
||||||
|
// and no upload has every height. Missing names return 403, so just walk
|
||||||
|
// the list until one loads. 720 first as a sane default for a 300px embed;
|
||||||
|
// 1080 last, only worth fetching when nothing smaller exists.
|
||||||
|
const HEIGHTS = [720, 480, 360, 270, 240, 220, 1080];
|
||||||
|
const FAMILIES = ['CMAF', 'DASH'];
|
||||||
|
const AUDIO_NAMES = [
|
||||||
|
'CMAF_AUDIO_128', 'CMAF_AUDIO_64',
|
||||||
|
'DASH_AUDIO_128', 'DASH_AUDIO_64', 'DASH_audio',
|
||||||
|
];
|
||||||
|
|
||||||
|
function mediaUrls(id, names) {
|
||||||
|
return names.map(name => 'https://v.redd.it/' + id + '/' + name + '.mp4');
|
||||||
|
}
|
||||||
|
|
||||||
|
function videoNames() {
|
||||||
|
let names = [];
|
||||||
|
for (let family of FAMILIES) {
|
||||||
|
for (let height of HEIGHTS) {
|
||||||
|
names.push(family + '_' + height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vredditId(url) {
|
||||||
|
let match = url.match(/^https?:\/\/v\.redd\.it\/([A-Za-z0-9]+)/i);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next candidate URL whenever the current one fails to load.
|
||||||
|
function chainSources(media, sources, onExhausted) {
|
||||||
|
let index = 0;
|
||||||
|
media.src = sources[0];
|
||||||
|
media.addEventListener('error', function() {
|
||||||
|
index += 1;
|
||||||
|
if (index < sources.length) {
|
||||||
|
media.src = sources[index];
|
||||||
|
media.load();
|
||||||
|
} else if (onExhausted) {
|
||||||
|
onExhausted();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MP4 renditions carry the video track only; the audio is a separate
|
||||||
|
// file that has to be kept in step with the video element by hand.
|
||||||
|
function buildAudio(video, id) {
|
||||||
|
let audio = document.createElement('audio');
|
||||||
|
audio.preload = 'metadata';
|
||||||
|
chainSources(audio, mediaUrls(id, AUDIO_NAMES));
|
||||||
|
|
||||||
|
video.addEventListener('play', function() {
|
||||||
|
audio.currentTime = video.currentTime;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
});
|
||||||
|
video.addEventListener('pause', () => audio.pause());
|
||||||
|
video.addEventListener('seeking', () => { audio.currentTime = video.currentTime; });
|
||||||
|
video.addEventListener('ratechange', () => { audio.playbackRate = video.playbackRate; });
|
||||||
|
video.addEventListener('volumechange', function() {
|
||||||
|
audio.volume = video.volume;
|
||||||
|
audio.muted = video.muted;
|
||||||
|
});
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVideo(href) {
|
||||||
|
let container = document.createElement('span');
|
||||||
|
let mediaUrl = unwrapMediaUrl(href);
|
||||||
|
let id = vredditId(mediaUrl);
|
||||||
|
|
||||||
|
let video = document.createElement('video');
|
||||||
|
video.controls = true;
|
||||||
|
video.preload = 'metadata';
|
||||||
|
video.style = "max-width: 300px";
|
||||||
|
|
||||||
|
let sources = id ? mediaUrls(id, videoNames()) : [mediaUrl];
|
||||||
|
|
||||||
|
chainSources(video, sources, function() {
|
||||||
|
console.log("no playable source for " + href);
|
||||||
|
let anchor = document.createElement('a');
|
||||||
|
anchor.href = href;
|
||||||
|
anchor.target = '_blank';
|
||||||
|
anchor.textContent = '<video>';
|
||||||
|
container.replaceChildren(anchor);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(video);
|
||||||
|
if (id) {
|
||||||
|
container.appendChild(buildAudio(video, id));
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image inlining
|
||||||
|
for (var elem of collectLinks('<image>')) {
|
||||||
console.log("inlining image " + elem.href);
|
console.log("inlining image " + elem.href);
|
||||||
let image = new Image();
|
let image = new Image();
|
||||||
image.src = elem.href;
|
image.src = elem.href;
|
||||||
@@ -45,6 +162,13 @@
|
|||||||
elem.remove();
|
elem.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle video inlining
|
||||||
|
for (var videoElem of collectLinks('<video>')) {
|
||||||
|
console.log("inlining video " + videoElem.href);
|
||||||
|
videoElem.parentElement.appendChild(buildVideo(videoElem.href));
|
||||||
|
videoElem.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// Only proceed with link rewriting if we're not already in a comments section
|
// Only proceed with link rewriting if we're not already in a comments section
|
||||||
if (!window.location.pathname.includes('/comments/')) {
|
if (!window.location.pathname.includes('/comments/')) {
|
||||||
// Handle post link redirection
|
// Handle post link redirection
|
||||||
|
|||||||
Reference in New Issue
Block a user