Compare commits
17 Commits
60f86652f7
...
master
Author | SHA1 | Date | |
---|---|---|---|
f668f59f70 | |||
91a9c69cba | |||
2b00bec445 | |||
54c6740dfe | |||
5ef8692a3a | |||
e5e1987bd1 | |||
ff3b2765a7 | |||
![]() |
7c69d73083 | ||
![]() |
90c3398ba3 | ||
![]() |
9abe867c31 | ||
![]() |
ccfaefbf46 | ||
![]() |
a1e889778c | ||
![]() |
01bd11a942 | ||
![]() |
b4aaa4410c | ||
![]() |
010c6bd191 | ||
![]() |
14adf10c68 | ||
![]() |
90f86feb39 |
33
.build.yaml
33
.build.yaml
@@ -1,33 +0,0 @@
|
|||||||
image: debian/bookworm
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
- a19cbc4c-b6a1-414e-bf1c-1e06abc684eb
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- print-env: |
|
|
||||||
echo "$JOB_ID"
|
|
||||||
echo "$JOB_URL"
|
|
||||||
echo "$BUILD_SUBMITTER"
|
|
||||||
echo "$BUILD_REASON"
|
|
||||||
echo "$GIT_REF"
|
|
||||||
- setup-env: |
|
|
||||||
sudo timedatectl set-timezone America/Denver
|
|
||||||
echo "SLUG=ghcr.io/cwpearson/ytdlp-site" >> ~/.buildenv
|
|
||||||
echo "DATE=$(date +"%Y%m%d_%H%M")" >> ~/.buildenv
|
|
||||||
- prerequisites: |
|
|
||||||
bash ytdlp-site/.ci/debian_setup_docker.sh
|
|
||||||
- build: |
|
|
||||||
cd ytdlp-site
|
|
||||||
docker build . --file Dockerfile --build-arg GIT_SHA=$(git rev-parse HEAD) --tag "$SLUG:$DATE" --tag "$SLUG:latest"
|
|
||||||
- deploy: |
|
|
||||||
if [ "$GIT_REF" != "refs/heads/master" ]; then exit 0; fi
|
|
||||||
set +x
|
|
||||||
cat ~/.ghcr_token | docker login ghcr.io -u cwpearson --password-stdin
|
|
||||||
set -x
|
|
||||||
docker push "$SLUG:latest"
|
|
||||||
docker push "$SLUG:$DATE"
|
|
||||||
|
|
||||||
triggers:
|
|
||||||
- action: email
|
|
||||||
condition: failure
|
|
||||||
to: Carl Pearson <srht@carlpearson.net>
|
|
17
.ci/ubuntu_setup_docker.sh
Normal file
17
.ci/ubuntu_setup_docker.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Add Docker's official GPG key:
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install ca-certificates curl
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
# Add the repository to Apt sources:
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
sudo usermod -aG docker $(whoami)
|
55
.gitea/workflows/build-deploy.yml
Normal file
55
.gitea/workflows/build-deploy.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Build and Deploy ytdlp-site
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
# Runs every Friday at 2:30 AM UTC
|
||||||
|
- cron: '30 2 * * 5'
|
||||||
|
env:
|
||||||
|
SLUG: git.carlpearson.net/cwpearson/ytdlp-site
|
||||||
|
TZ: America/Denver
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Print environment info
|
||||||
|
run: |
|
||||||
|
echo "Job ID: ${{ github.run_id }}"
|
||||||
|
echo "Job URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
echo "Build Submitter: ${{ github.actor }}"
|
||||||
|
echo "Build Reason: ${{ github.event_name }}"
|
||||||
|
echo "Git Ref: ${{ github.ref }}"
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: |
|
||||||
|
echo "DATE=$(date +"%Y%m%d_%H%M")" >> $GITHUB_ENV
|
||||||
|
echo "GIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
run: |
|
||||||
|
bash .ci/ubuntu_setup_docker.sh
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build . \
|
||||||
|
--file Dockerfile \
|
||||||
|
--build-arg GIT_SHA=${{ env.GIT_SHA }} \
|
||||||
|
--tag ${{ env.SLUG }}:${{ env.DATE }} \
|
||||||
|
--tag ${{ env.SLUG }}:latest
|
||||||
|
|
||||||
|
- name: Deploy to Container Registry
|
||||||
|
if: gitea.ref == 'refs/heads/master' && gitea.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GIT_PASSWORD }}" | docker login git.carlpearson.net -u "${{ secrets.GIT_USERNAME }}" --password-stdin
|
||||||
|
docker push ${{ env.SLUG }}:latest
|
||||||
|
docker push ${{ env.SLUG }}:${{ env.DATE }}
|
@@ -13,10 +13,10 @@ ADD ffmpeg /src/ffmpeg
|
|||||||
ADD handlers /src/handlers
|
ADD handlers /src/handlers
|
||||||
ADD media /src/media
|
ADD media /src/media
|
||||||
ADD originals /src/originals
|
ADD originals /src/originals
|
||||||
Add playlists /src/playlists
|
ADD playlists /src/playlists
|
||||||
ADD transcodes /src/transcodes
|
ADD transcodes /src/transcodes
|
||||||
ADD users /src/users
|
ADD users /src/users
|
||||||
Add ytdlp /src/ytdlp
|
ADD ytdlp /src/ytdlp
|
||||||
ADD go.mod /src/.
|
ADD go.mod /src/.
|
||||||
|
|
||||||
RUN cd /src && go mod tidy
|
RUN cd /src && go mod tidy
|
||||||
|
10
README.md
10
README.md
@@ -49,6 +49,10 @@ Build and push this container to ghcr
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
- Video Page
|
||||||
|
- [ ] Link to author
|
||||||
|
- Videos Page
|
||||||
|
- [x] Link to author
|
||||||
- [ ] License
|
- [ ] License
|
||||||
- [x] Delete failed videos
|
- [x] Delete failed videos
|
||||||
- [ ] edit original metadata
|
- [ ] edit original metadata
|
||||||
@@ -62,16 +66,14 @@ Build and push this container to ghcr
|
|||||||
- [x] `ffmpeg` version
|
- [x] `ffmpeg` version
|
||||||
- [x] `yt-dlp` version
|
- [x] `yt-dlp` version
|
||||||
- [x] disk space
|
- [x] disk space
|
||||||
- [ ] add progress bar to represent usage
|
- [x] add progress bar to represent usage
|
||||||
- [ ] CPU history
|
- [ ] CPU history
|
||||||
- [ ] skip buttons for audio player
|
- [ ] skip buttons for audio player
|
||||||
- [ ] Track progress via video URL rather than ID
|
- [ ] Track progress via video URL rather than ID
|
||||||
- [ ] Show author on video page
|
|
||||||
- [ ] Link to author on video page
|
|
||||||
- [ ] Link to author on videos page
|
|
||||||
- [ ] video clips
|
- [ ] video clips
|
||||||
- [ ] move original video to bottom
|
- [ ] move original video to bottom
|
||||||
- [x] sort videos most to least recent
|
- [x] sort videos most to least recent
|
||||||
- [x] header on playlist page
|
- [x] header on playlist page
|
||||||
- [x] Choose database directory
|
- [x] Choose database directory
|
||||||
- [x] add extension to download
|
- [x] add extension to download
|
||||||
|
- [x] server-side watch progress
|
||||||
|
42
handlers.go
42
handlers.go
@@ -115,8 +115,10 @@ func downloadPostHandler(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
title string
|
title string
|
||||||
artist string
|
artist string
|
||||||
|
uploadDate time.Time
|
||||||
|
uploaderUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getYtdlpTitle(url string, args []string) (string, error) {
|
func getYtdlpTitle(url string, args []string) (string, error) {
|
||||||
@@ -172,8 +174,26 @@ func getYtdlpArtist(url string, args []string) (string, error) {
|
|||||||
return strings.TrimSpace(string(stdout)), nil
|
return strings.TrimSpace(string(stdout)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getYtdlpExt(url string, args []string) (string, error) {
|
func getYtdlpUploadDate(url string, args []string) (time.Time, error) {
|
||||||
args = append(args, "--simulate", "--print", "%(ext)s", url)
|
args = append(args, "--simulate", "--print", "%(upload_date>%Y-%m-%d)s", url)
|
||||||
|
stdout, _, err := ytdlp.Run(args...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dateStr := strings.TrimSpace(string(stdout))
|
||||||
|
|
||||||
|
uploadDate, err := time.Parse("2006-01-02", dateStr)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("failed to parse date '%s': %w", dateStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getYtdlpUploaderUrl(url string, args []string) (string, error) {
|
||||||
|
args = append(args, "--simulate", "--print", "%(uploader_url)s", url)
|
||||||
stdout, _, err := ytdlp.Run(args...)
|
stdout, _, err := ytdlp.Run(args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
@@ -195,6 +215,14 @@ func getYtdlpMeta(url string, args []string) (Meta, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return meta, err
|
return meta, err
|
||||||
}
|
}
|
||||||
|
meta.uploadDate, err = getYtdlpUploadDate(url, args)
|
||||||
|
if err != nil {
|
||||||
|
return meta, err
|
||||||
|
}
|
||||||
|
meta.uploaderUrl, err = getYtdlpUploaderUrl(url, args)
|
||||||
|
if err != nil {
|
||||||
|
return meta, err
|
||||||
|
}
|
||||||
|
|
||||||
return meta, nil
|
return meta, nil
|
||||||
}
|
}
|
||||||
@@ -556,8 +584,10 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
|
|||||||
}
|
}
|
||||||
log.Debugf("original metadata %v", origMeta)
|
log.Debugf("original metadata %v", origMeta)
|
||||||
err = db.Model(&originals.Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{
|
err = db.Model(&originals.Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{
|
||||||
"title": origMeta.title,
|
"title": origMeta.title,
|
||||||
"artist": origMeta.artist,
|
"artist": origMeta.artist,
|
||||||
|
"upload_date": origMeta.uploadDate,
|
||||||
|
"uploader_url": origMeta.uploaderUrl,
|
||||||
}).Error
|
}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("couldn't store metadata:", err)
|
log.Errorln("couldn't store metadata:", err)
|
||||||
|
54
handlers/set_timestamp.go
Normal file
54
handlers/set_timestamp.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"ytdlp-site/database"
|
||||||
|
"ytdlp-site/originals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetTimestamp(c echo.Context) error {
|
||||||
|
// Parse the ID from the URL parameter
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID format"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a struct to receive the timestamp from JSON
|
||||||
|
type TimestampRequest struct {
|
||||||
|
Timestamp float64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request body
|
||||||
|
var req TimestampRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugln("set video", id, "timestamp:", req.Timestamp)
|
||||||
|
|
||||||
|
// Update the timestamp field with the value from the request
|
||||||
|
db := database.Get()
|
||||||
|
result := db.Model(&originals.Original{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("timestamp", req.Timestamp)
|
||||||
|
|
||||||
|
// Handle database errors
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Errorln(result.Error)
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Database error"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if record was found
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
log.Errorln(gorm.ErrRecordNotFound)
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Record not found"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
1
main.go
1
main.go
@@ -144,6 +144,7 @@ func main() {
|
|||||||
e.GET("/temp/:token", tempHandler)
|
e.GET("/temp/:token", tempHandler)
|
||||||
e.POST("/video/:id/process", processHandler, handlers.AuthMiddleware)
|
e.POST("/video/:id/process", processHandler, handlers.AuthMiddleware)
|
||||||
e.POST("/video/:id/toggle_watched", handlers.ToggleWatched, handlers.AuthMiddleware)
|
e.POST("/video/:id/toggle_watched", handlers.ToggleWatched, handlers.AuthMiddleware)
|
||||||
|
e.POST("/video/:id/set_timestamp", handlers.SetTimestamp, handlers.AuthMiddleware)
|
||||||
e.POST("/delete_video/:id", deleteVideoHandler, handlers.AuthMiddleware)
|
e.POST("/delete_video/:id", deleteVideoHandler, handlers.AuthMiddleware)
|
||||||
e.POST("/delete_audio/:id", deleteAudioHandler, handlers.AuthMiddleware)
|
e.POST("/delete_audio/:id", deleteAudioHandler, handlers.AuthMiddleware)
|
||||||
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, handlers.AuthMiddleware)
|
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, handlers.AuthMiddleware)
|
||||||
|
@@ -2,6 +2,7 @@ package originals
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
"ytdlp-site/database"
|
"ytdlp-site/database"
|
||||||
"ytdlp-site/transcodes"
|
"ytdlp-site/transcodes"
|
||||||
|
|
||||||
@@ -23,14 +24,17 @@ const (
|
|||||||
|
|
||||||
type Original struct {
|
type Original struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
UserID uint
|
UserID uint
|
||||||
URL string
|
URL string
|
||||||
Title string
|
Title string
|
||||||
Artist string
|
Artist string
|
||||||
Status Status
|
UploadDate time.Time
|
||||||
Audio bool // video download requested
|
UploaderUrl string
|
||||||
Video bool // audio download requested
|
Status Status
|
||||||
Watched bool
|
Audio bool // video download requested
|
||||||
|
Video bool // audio download requested
|
||||||
|
Watched bool
|
||||||
|
Timestamp float64
|
||||||
|
|
||||||
Playlist bool // part of a playlist
|
Playlist bool // part of a playlist
|
||||||
PlaylistID uint // Playlist.ID (if part of a playlist)
|
PlaylistID uint // Playlist.ID (if part of a playlist)
|
||||||
|
@@ -1,42 +1,99 @@
|
|||||||
|
|
||||||
// Get all video and audio elements
|
// Get all video and audio elements
|
||||||
const mediaElements = document.querySelectorAll('video, audio');
|
const mediaElements = document.querySelectorAll('video, audio');
|
||||||
|
|
||||||
// Generate a unique key for this page
|
// Get the video ID from the hidden input
|
||||||
const pageKey = `mediaProgress_${window.location.pathname}`;
|
const videoId = document.getElementById('video-id').value;
|
||||||
|
|
||||||
// Function to save the current time of the most recently played media
|
// Get the initial timestamp from the hidden input
|
||||||
function saveMediaProgress(media) {
|
const initialTimestamp = parseFloat(document.getElementById('video-timestamp').value || 0);
|
||||||
localStorage.setItem(pageKey, media.currentTime);
|
|
||||||
|
// Track which media element is currently playing
|
||||||
|
let activeMedia = null;
|
||||||
|
let updateTimer = null;
|
||||||
|
const UPDATE_INTERVAL = 5000; // 5 seconds in milliseconds
|
||||||
|
|
||||||
|
// Function to send the current time to the server
|
||||||
|
function sendTimestampToServer(timestamp) {
|
||||||
|
console.log(videoId, "->", timestamp)
|
||||||
|
fetch(`/video/${videoId}/set_timestamp`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ "timestamp": timestamp })
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error sending timestamp to server:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load and set the saved time for all media elements
|
// Function to start the update timer
|
||||||
function loadMediaProgress() {
|
function startUpdateTimer() {
|
||||||
const savedTime = localStorage.getItem(pageKey);
|
// Clear any existing timer
|
||||||
if (savedTime) {
|
stopUpdateTimer();
|
||||||
mediaElements.forEach(media => {
|
|
||||||
media.currentTime = Math.min(parseFloat(savedTime), media.duration || Infinity);
|
// Start a new timer that sends updates every 5 seconds
|
||||||
});
|
updateTimer = setInterval(() => {
|
||||||
|
if (activeMedia && !activeMedia.paused) {
|
||||||
|
sendTimestampToServer(activeMedia.currentTime);
|
||||||
|
}
|
||||||
|
}, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop the update timer
|
||||||
|
function stopUpdateTimer() {
|
||||||
|
if (updateTimer) {
|
||||||
|
clearInterval(updateTimer);
|
||||||
|
updateTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up event listeners for each media element
|
// Set up event listeners for each media element
|
||||||
mediaElements.forEach(media => {
|
mediaElements.forEach(media => {
|
||||||
// Save progress when the media is playing
|
// Set initial timestamp when media is ready
|
||||||
media.addEventListener('timeupdate', () => {
|
media.addEventListener('loadedmetadata', () => {
|
||||||
if (!media.paused) {
|
// Only set if the initial timestamp is within the media duration
|
||||||
saveMediaProgress(media);
|
if (initialTimestamp > 0 && initialTimestamp < media.duration) {
|
||||||
|
media.currentTime = initialTimestamp;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also save when the media is paused
|
// When media starts playing
|
||||||
media.addEventListener('pause', () => saveMediaProgress(media));
|
media.addEventListener('play', () => {
|
||||||
|
// Set this as the active media
|
||||||
// Load the saved progress when the media is ready
|
activeMedia = media;
|
||||||
media.addEventListener('loadedmetadata', loadMediaProgress);
|
// Start the update timer
|
||||||
|
startUpdateTimer();
|
||||||
// Clear progress when any media ends
|
|
||||||
media.addEventListener('ended', () => {
|
|
||||||
localStorage.removeItem(pageKey);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// When media is paused
|
||||||
|
media.addEventListener('pause', () => {
|
||||||
|
// Send the current timestamp
|
||||||
|
sendTimestampToServer(media.currentTime);
|
||||||
|
|
||||||
|
// If this is the active media, stop the timer
|
||||||
|
if (activeMedia === media) {
|
||||||
|
stopUpdateTimer();
|
||||||
|
activeMedia = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset timestamp when media ends
|
||||||
|
media.addEventListener('ended', () => {
|
||||||
|
sendTimestampToServer(0);
|
||||||
|
|
||||||
|
// If this is the active media, stop the timer
|
||||||
|
if (activeMedia === media) {
|
||||||
|
stopUpdateTimer();
|
||||||
|
activeMedia = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when the page is unloaded
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
// Send final timestamp if media is playing
|
||||||
|
if (activeMedia && !activeMedia.paused) {
|
||||||
|
sendTimestampToServer(activeMedia.currentTime);
|
||||||
|
}
|
||||||
|
stopUpdateTimer();
|
||||||
|
});
|
||||||
|
@@ -10,4 +10,12 @@
|
|||||||
.video-list {
|
.video-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-artist-link {
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="build-id">
|
<div class="build-id">
|
||||||
Build ID: <a
|
Build ID: <a
|
||||||
href="https://github.com/cwpearson/ytdlp-site/compare/{{.Footer.BuildId}}..master">{{.Footer.BuildIdShort}}</a>
|
href="https://git.sr.ht/~cwpearson/ytdlp-site/commit/{{.Footer.BuildId}}">{{.Footer.BuildIdShort}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -56,6 +56,7 @@
|
|||||||
<select name="fps" id="fps-select">
|
<select name="fps" id="fps-select">
|
||||||
<option value="24">24 fps</option>
|
<option value="24">24 fps</option>
|
||||||
<option value="25">25 fps</option>
|
<option value="25">25 fps</option>
|
||||||
|
<option value="29.97">29.97 fps</option>
|
||||||
<option value="30">30 fps</option>
|
<option value="30">30 fps</option>
|
||||||
<option value="59.94">59.94 fps</option>
|
<option value="59.94">59.94 fps</option>
|
||||||
<option value="60">60 fps</option>
|
<option value="60">60 fps</option>
|
||||||
@@ -104,7 +105,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="video-id" value="{{.original.ID}}">
|
||||||
|
<input type="hidden" id="video-timestamp" value="{{.original.Timestamp}}">
|
||||||
<script src="/static/script/save-media-progress.js"></script>
|
<script src="/static/script/save-media-progress.js"></script>
|
||||||
|
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
|
@@ -35,7 +35,11 @@
|
|||||||
<div class="video-title video-title-bare {{$bareHidden}}">
|
<div class="video-title video-title-bare {{$bareHidden}}">
|
||||||
{{.Title}}
|
{{.Title}}
|
||||||
</div>
|
</div>
|
||||||
<div class="video-info">{{.Artist}}</div>
|
{{if (eq .UploaderUrl "")}}
|
||||||
|
<div class="video-info video-artist-bare">{{.Artist}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="video-info video-artist-link"><a href="{{.UploaderUrl}}">{{.Artist}}</a></div>
|
||||||
|
{{end}}
|
||||||
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
|
<div class="video-info"><a href="{{.URL}}">{{.URL}}</a></div>
|
||||||
<div class="video-info video-status">{{.Status}}</div>
|
<div class="video-info video-status">{{.Status}}</div>
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
|
Reference in New Issue
Block a user