Compare commits
10 Commits
60f86652f7
...
7c69d73083
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c69d73083 | ||
![]() |
90c3398ba3 | ||
![]() |
9abe867c31 | ||
![]() |
ccfaefbf46 | ||
![]() |
a1e889778c | ||
![]() |
01bd11a942 | ||
![]() |
b4aaa4410c | ||
![]() |
010c6bd191 | ||
![]() |
14adf10c68 | ||
![]() |
90f86feb39 |
10
README.md
10
README.md
@@ -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
|
||||
|
42
handlers.go
42
handlers.go
@@ -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
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.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)
|
||||
|
@@ -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)
|
||||
|
@@ -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();
|
||||
});
|
||||
|
@@ -10,4 +10,12 @@
|
||||
.video-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.video-artist-link {
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: gray;
|
||||
}
|
||||
}
|
@@ -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}}
|
||||
|
@@ -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" .}}
|
||||
|
@@ -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">
|
||||
|
Reference in New Issue
Block a user