Compare commits

...

10 Commits

Author SHA1 Message Date
Carl Pearson
7c69d73083 Update readme 2025-05-02 05:55:33 -06:00
Carl Pearson
90c3398ba3 Add Artist link 2025-05-02 05:51:52 -06:00
Carl Pearson
9abe867c31 Retrieve upload URL 2025-05-02 05:39:15 -06:00
Carl Pearson
ccfaefbf46 remove unused function 2025-05-02 05:26:15 -06:00
Carl Pearson
a1e889778c github -> git.sr.ht 2025-05-02 05:14:53 -06:00
Carl Pearson
01bd11a942 reduce CPU usage 2025-05-01 05:56:04 -06:00
Carl Pearson
b4aaa4410c Track timestamps server-side 2025-05-01 05:50:53 -06:00
Carl Pearson
010c6bd191 Add /video/id/set_timestamp handler, timestamp in video HTML 2025-05-01 05:33:22 -06:00
Carl Pearson
14adf10c68 Add video-id to video page 2025-05-01 05:31:19 -06:00
Carl Pearson
90f86feb39 add 29.97 fps 2025-04-30 05:32:07 -06:00
10 changed files with 210 additions and 48 deletions

View File

@@ -49,6 +49,10 @@ Build and push this container to ghcr
## Roadmap
- Video Page
- [ ] Link to author
- Videos Page
- [x] Link to author
- [ ] License
- [x] Delete failed videos
- [ ] edit original metadata
@@ -62,16 +66,14 @@ Build and push this container to ghcr
- [x] `ffmpeg` version
- [x] `yt-dlp` version
- [x] disk space
- [ ] add progress bar to represent usage
- [x] add progress bar to represent usage
- [ ] CPU history
- [ ] skip buttons for audio player
- [ ] 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
- [ ] move original video to bottom
- [x] sort videos most to least recent
- [x] header on playlist page
- [x] Choose database directory
- [x] add extension to download
- [x] server-side watch progress

View File

@@ -115,8 +115,10 @@ func downloadPostHandler(c echo.Context) error {
}
type Meta struct {
title string
artist string
title string
artist string
uploadDate time.Time
uploaderUrl string
}
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
}
func getYtdlpExt(url string, args []string) (string, error) {
args = append(args, "--simulate", "--print", "%(ext)s", url)
func getYtdlpUploadDate(url string, args []string) (time.Time, error) {
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...)
if err != nil {
log.Errorln(err)
@@ -195,6 +215,14 @@ func getYtdlpMeta(url string, args []string) (Meta, error) {
if err != nil {
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
}
@@ -556,8 +584,10 @@ func startDownload(originalID uint, videoURL string, audioOnly bool) {
}
log.Debugf("original metadata %v", origMeta)
err = db.Model(&originals.Original{}).Where("id = ?", originalID).Updates(map[string]interface{}{
"title": origMeta.title,
"artist": origMeta.artist,
"title": origMeta.title,
"artist": origMeta.artist,
"upload_date": origMeta.uploadDate,
"uploader_url": origMeta.uploaderUrl,
}).Error
if err != nil {
log.Errorln("couldn't store metadata:", err)

54
handlers/set_timestamp.go Normal file
View 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)
}

View File

@@ -144,6 +144,7 @@ func main() {
e.GET("/temp/:token", tempHandler)
e.POST("/video/:id/process", processHandler, 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_audio/:id", deleteAudioHandler, handlers.AuthMiddleware)
e.POST("/transcode_to_video/:id", transcodeToVideoHandler, handlers.AuthMiddleware)

View File

@@ -2,6 +2,7 @@ package originals
import (
"sync"
"time"
"ytdlp-site/database"
"ytdlp-site/transcodes"
@@ -23,14 +24,17 @@ const (
type Original struct {
gorm.Model
UserID uint
URL string
Title string
Artist string
Status Status
Audio bool // video download requested
Video bool // audio download requested
Watched bool
UserID uint
URL string
Title string
Artist string
UploadDate time.Time
UploaderUrl string
Status Status
Audio bool // video download requested
Video bool // audio download requested
Watched bool
Timestamp float64
Playlist bool // part of a playlist
PlaylistID uint // Playlist.ID (if part of a playlist)

View File

@@ -1,42 +1,99 @@
// Get all video and audio elements
const mediaElements = document.querySelectorAll('video, audio');
// Generate a unique key for this page
const pageKey = `mediaProgress_${window.location.pathname}`;
// Get the video ID from the hidden input
const videoId = document.getElementById('video-id').value;
// Function to save the current time of the most recently played media
function saveMediaProgress(media) {
localStorage.setItem(pageKey, media.currentTime);
// Get the initial timestamp from the hidden input
const initialTimestamp = parseFloat(document.getElementById('video-timestamp').value || 0);
// 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 loadMediaProgress() {
const savedTime = localStorage.getItem(pageKey);
if (savedTime) {
mediaElements.forEach(media => {
media.currentTime = Math.min(parseFloat(savedTime), media.duration || Infinity);
});
// Function to start the update timer
function startUpdateTimer() {
// Clear any existing timer
stopUpdateTimer();
// 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
mediaElements.forEach(media => {
// Save progress when the media is playing
media.addEventListener('timeupdate', () => {
if (!media.paused) {
saveMediaProgress(media);
// Set initial timestamp when media is ready
media.addEventListener('loadedmetadata', () => {
// Only set if the initial timestamp is within the media duration
if (initialTimestamp > 0 && initialTimestamp < media.duration) {
media.currentTime = initialTimestamp;
}
});
// Also save when the media is paused
media.addEventListener('pause', () => saveMediaProgress(media));
// Load the saved progress when the media is ready
media.addEventListener('loadedmetadata', loadMediaProgress);
// Clear progress when any media ends
media.addEventListener('ended', () => {
localStorage.removeItem(pageKey);
// When media starts playing
media.addEventListener('play', () => {
// Set this as the active media
activeMedia = media;
// Start the update timer
startUpdateTimer();
});
});
// 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();
});

View File

@@ -10,4 +10,12 @@
.video-list {
grid-template-columns: 1fr;
}
}
.video-artist-link {
a,
a:visited {
color: gray;
}
}

View File

@@ -5,7 +5,7 @@
</div>
<div class="build-id">
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>
{{end}}

View File

@@ -56,6 +56,7 @@
<select name="fps" id="fps-select">
<option value="24">24 fps</option>
<option value="25">25 fps</option>
<option value="29.97">29.97 fps</option>
<option value="30">30 fps</option>
<option value="59.94">59.94 fps</option>
<option value="60">60 fps</option>
@@ -104,7 +105,8 @@
</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>
{{template "footer" .}}

View File

@@ -35,7 +35,11 @@
<div class="video-title video-title-bare {{$bareHidden}}">
{{.Title}}
</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 video-status">{{.Status}}</div>
<div class="video-info">