522 lines
14 KiB
Go
522 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"govd/enums"
|
|
|
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
|
"github.com/google/uuid"
|
|
"github.com/guregu/null/v6/zero"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
fileTypeDocument = "document"
|
|
fileTypePhoto = "photo"
|
|
fileTypeVideo = "video"
|
|
fileTypeAudio = "audio"
|
|
|
|
fileExtMP4 = "mp4"
|
|
fileExtWebM = "webm"
|
|
fileExtMP3 = "mp3"
|
|
fileExtM4A = "m4a"
|
|
fileExtFLAC = "flac"
|
|
fileExtOGG = "oga"
|
|
fileExtJPEG = "jpeg"
|
|
fileExtWebP = "webp"
|
|
fileExtJPG = "jpg"
|
|
fileExtGIF = "gif"
|
|
fileExtOGV = "ogv"
|
|
fileExtAVI = "avi"
|
|
fileExtMKV = "mkv"
|
|
fileExtMOV = "mov"
|
|
)
|
|
|
|
type Media struct {
|
|
ID uint `json:"-"`
|
|
ContentID string `gorm:"not null;index" json:"content_id"`
|
|
ContentURL string `gorm:"not null" json:"content_url"`
|
|
ExtractorCodeName string `gorm:"not null;index" json:"extractor_code_name"`
|
|
Caption zero.String `json:"caption"`
|
|
NSFW bool `gorm:"default:false" json:"nsfw"`
|
|
CreatedAt time.Time `json:"-"`
|
|
UpdatedAt time.Time `json:"-"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
|
|
Format *MediaFormat `json:"-"`
|
|
|
|
Formats []*MediaFormat `gorm:"-" json:"formats"`
|
|
}
|
|
|
|
type MediaFormat struct {
|
|
ID uint `json:"-"`
|
|
MediaID uint `gorm:"index:idx_media_format,priority:1;not null" json:"-"`
|
|
Type enums.MediaType `gorm:"not null;index:idx_media_type" json:"type"`
|
|
FormatID string `gorm:"not null;index" json:"format_id"`
|
|
FileID string `gorm:"not null;index" json:"-"`
|
|
VideoCodec enums.MediaCodec `json:"video_codec"`
|
|
AudioCodec enums.MediaCodec `json:"audio_codec"`
|
|
Duration int64 `json:"duration"`
|
|
Width int64 `json:"width"`
|
|
Height int64 `json:"height"`
|
|
Bitrate int64 `json:"bitrate"`
|
|
Title string `json:"title"`
|
|
Artist string `json:"artist"`
|
|
IsDefault bool `gorm:"default:false;index" json:"is_default"`
|
|
Segments []string `gorm:"-" json:"segments"`
|
|
FileSize int64 `json:"-"`
|
|
Plugins []Plugin `gorm:"-" json:"-"`
|
|
|
|
CreatedAt time.Time `json:"-"`
|
|
UpdatedAt time.Time `json:"-"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
|
|
// api use only, not stored in database
|
|
URL []string `gorm:"-" json:"url"`
|
|
Thumbnail []string `gorm:"-" json:"thumbnail"`
|
|
|
|
Media *Media `gorm:"foreignKey:MediaID" json:"-"`
|
|
}
|
|
|
|
type DownloadedMedia struct {
|
|
FilePath string
|
|
ThumbnailFilePath string
|
|
Media *Media
|
|
Index int
|
|
}
|
|
|
|
func (media *Media) GetFormat(formatID string) *MediaFormat {
|
|
for _, format := range media.Formats {
|
|
if format.FormatID == formatID {
|
|
return format
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (media *Media) GetDefaultFormat() *MediaFormat {
|
|
format := media.GetDefaultVideoFormat()
|
|
if format != nil {
|
|
return format
|
|
}
|
|
format = media.GetDefaultAudioFormat()
|
|
if format != nil {
|
|
return format
|
|
}
|
|
format = media.GetDefaultPhotoFormat()
|
|
if format != nil {
|
|
return format
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (media *Media) GetDefaultVideoFormat() *MediaFormat {
|
|
filtered := filterFormats(media.Formats, func(format *MediaFormat) bool {
|
|
return format.VideoCodec == enums.MediaCodecAVC
|
|
})
|
|
if len(filtered) == 0 {
|
|
filtered = filterFormats(media.Formats, func(format *MediaFormat) bool {
|
|
return format.VideoCodec != ""
|
|
})
|
|
}
|
|
if len(filtered) == 0 {
|
|
return nil
|
|
}
|
|
slices.SortFunc(filtered, func(a, b *MediaFormat) int {
|
|
if a.Bitrate != b.Bitrate {
|
|
if a.Bitrate > b.Bitrate {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
if a.Height > b.Height {
|
|
return -1
|
|
} else if a.Height < b.Height {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
bestFormat := filtered[0]
|
|
bestFormat.IsDefault = true
|
|
return bestFormat
|
|
}
|
|
|
|
func (media *Media) GetDefaultAudioFormat() *MediaFormat {
|
|
filtered := filterFormats(media.Formats, func(format *MediaFormat) bool {
|
|
return format.VideoCodec == "" &&
|
|
(format.AudioCodec == enums.MediaCodecAAC ||
|
|
format.AudioCodec == enums.MediaCodecMP3)
|
|
})
|
|
if len(filtered) == 0 {
|
|
filtered = filterFormats(media.Formats, func(format *MediaFormat) bool {
|
|
return format.VideoCodec == "" && format.AudioCodec != ""
|
|
})
|
|
}
|
|
if len(filtered) == 0 {
|
|
return nil
|
|
}
|
|
bestFormat := filtered[0]
|
|
for _, format := range filtered {
|
|
if format.Bitrate > bestFormat.Bitrate {
|
|
bestFormat = format
|
|
}
|
|
}
|
|
bestFormat.IsDefault = true
|
|
return bestFormat
|
|
}
|
|
|
|
func (media *Media) GetDefaultPhotoFormat() *MediaFormat {
|
|
filtered := filterFormats(media.Formats, func(format *MediaFormat) bool {
|
|
return format.Type == enums.MediaTypePhoto
|
|
})
|
|
if len(filtered) == 0 {
|
|
return nil
|
|
}
|
|
filtered[0].IsDefault = true
|
|
return filtered[0]
|
|
}
|
|
|
|
func (media *Media) GetAudioFromVideoFormat() *MediaFormat {
|
|
videoFormat := media.GetDefaultVideoFormat()
|
|
|
|
if videoFormat == nil {
|
|
return nil
|
|
}
|
|
|
|
return &MediaFormat{
|
|
Type: enums.MediaTypeAudio,
|
|
FormatID: "AudioFromVideo",
|
|
URL: videoFormat.URL,
|
|
AudioCodec: enums.MediaCodecAAC,
|
|
Thumbnail: videoFormat.Thumbnail,
|
|
Duration: videoFormat.Duration,
|
|
Title: videoFormat.Title,
|
|
Artist: videoFormat.Artist,
|
|
}
|
|
}
|
|
|
|
func (media *Media) SetCaption(caption string) {
|
|
if len(caption) == 0 {
|
|
return
|
|
}
|
|
media.Caption = zero.StringFrom(caption)
|
|
}
|
|
|
|
func (media *Media) AddFormat(fmt *MediaFormat) {
|
|
media.Formats = append(media.Formats, fmt)
|
|
}
|
|
|
|
func (media *Media) GetSortedFormats() []*MediaFormat {
|
|
// group by video format (codec, width, height)
|
|
groupedVideos := make(map[[3]int64]*MediaFormat)
|
|
for _, format := range media.Formats {
|
|
if format.Type == enums.MediaTypeVideo {
|
|
key := [3]int64{
|
|
getCodecPriority(format.VideoCodec),
|
|
format.Width,
|
|
format.Height,
|
|
}
|
|
existing, ok := groupedVideos[key]
|
|
if !ok || format.Bitrate > existing.Bitrate {
|
|
groupedVideos[key] = format
|
|
}
|
|
}
|
|
}
|
|
|
|
// group by audio format (codec, bitrate)
|
|
groupedAudios := make(map[[2]int64]*MediaFormat)
|
|
for _, format := range media.Formats {
|
|
if format.Type == enums.MediaTypeAudio {
|
|
key := [2]int64{
|
|
getCodecPriority(format.AudioCodec),
|
|
format.Bitrate,
|
|
}
|
|
_, exists := groupedAudios[key]
|
|
if !exists {
|
|
groupedAudios[key] = format
|
|
}
|
|
}
|
|
}
|
|
|
|
// combine the best video and audio into a final list
|
|
finalSortedList := make([]*MediaFormat, 0, len(groupedVideos)+len(groupedAudios)+len(media.Formats))
|
|
for _, best := range groupedVideos {
|
|
finalSortedList = append(finalSortedList, best)
|
|
}
|
|
for _, best := range groupedAudios {
|
|
finalSortedList = append(finalSortedList, best)
|
|
}
|
|
|
|
for _, format := range media.Formats {
|
|
if format.Type != enums.MediaTypeVideo && format.Type != enums.MediaTypeAudio {
|
|
finalSortedList = append(finalSortedList, format) // for non-video and non-audio formats
|
|
}
|
|
}
|
|
|
|
// sort the final list
|
|
sort.Slice(finalSortedList, func(i, j int) bool {
|
|
a, b := finalSortedList[i], finalSortedList[j]
|
|
// compare by type priority (video, audio, photo, etc.)
|
|
if cmp := getTypePriority(a.Type) - getTypePriority(b.Type); cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
// compare by codec priority (for both video and audio)
|
|
if a.Type == enums.MediaTypeVideo {
|
|
if cmp := getCodecPriority(a.VideoCodec) - getCodecPriority(b.VideoCodec); cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
} else if a.Type == enums.MediaTypeAudio {
|
|
if cmp := getCodecPriority(a.AudioCodec) - getCodecPriority(b.AudioCodec); cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
}
|
|
// compare by width for videos
|
|
if cmp := a.Width - b.Width; cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
// compare by height for videos
|
|
if cmp := a.Height - b.Height; cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
// compare by bitrate (lower bitrate first)
|
|
return a.Bitrate-b.Bitrate < 0
|
|
})
|
|
|
|
return finalSortedList
|
|
}
|
|
|
|
func filterFormats(
|
|
formats []*MediaFormat,
|
|
condition func(*MediaFormat) bool,
|
|
) []*MediaFormat {
|
|
var filtered []*MediaFormat
|
|
for _, format := range formats {
|
|
if condition(format) {
|
|
filtered = append(filtered, format)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func getCodecPriority(codec enums.MediaCodec) int64 {
|
|
codecPriority := map[enums.MediaCodec]int64{
|
|
enums.MediaCodecAVC: 1,
|
|
enums.MediaCodecHEVC: 2,
|
|
enums.MediaCodecMP3: 3,
|
|
enums.MediaCodecAAC: 4,
|
|
}
|
|
return codecPriority[codec]
|
|
}
|
|
|
|
func getTypePriority(mediaType enums.MediaType) int64 {
|
|
typePriority := map[enums.MediaType]int64{
|
|
enums.MediaTypeVideo: 1,
|
|
enums.MediaTypeAudio: 2,
|
|
enums.MediaTypePhoto: 3,
|
|
}
|
|
return typePriority[mediaType]
|
|
}
|
|
|
|
// getFormatInfo returns the file extension and the InputMedia type.
|
|
func (format *MediaFormat) GetFormatInfo() (string, string) {
|
|
if format.Type == enums.MediaTypePhoto {
|
|
return fileExtJPEG, fileTypePhoto
|
|
}
|
|
|
|
videoCodec := format.VideoCodec
|
|
audioCodec := format.AudioCodec
|
|
|
|
switch {
|
|
case videoCodec == enums.MediaCodecAVC && audioCodec == enums.MediaCodecAAC:
|
|
return fileExtMP4, fileTypeVideo
|
|
case videoCodec == enums.MediaCodecAVC && audioCodec == enums.MediaCodecMP3:
|
|
return fileExtMP4, fileTypeVideo
|
|
case videoCodec == enums.MediaCodecHEVC && audioCodec == enums.MediaCodecAAC:
|
|
return fileExtMP4, fileTypeDocument
|
|
case videoCodec == enums.MediaCodecHEVC && audioCodec == enums.MediaCodecMP3:
|
|
return fileExtMP4, fileTypeDocument
|
|
case videoCodec == enums.MediaCodecAVC && audioCodec == "":
|
|
return fileExtMP4, fileTypeVideo
|
|
case videoCodec == enums.MediaCodecHEVC && audioCodec == "":
|
|
return fileExtMP4, fileTypeDocument
|
|
case videoCodec == enums.MediaCodecWebP && audioCodec == "":
|
|
return fileExtWebP, fileTypeVideo
|
|
case videoCodec == "" && audioCodec == enums.MediaCodecMP3:
|
|
return fileExtMP3, fileTypeAudio
|
|
case videoCodec == "" && audioCodec == enums.MediaCodecAAC:
|
|
return fileExtM4A, fileTypeAudio
|
|
case videoCodec == "" && audioCodec == enums.MediaCodecFLAC:
|
|
return fileExtFLAC, fileTypeDocument
|
|
case videoCodec == "" && audioCodec == enums.MediaCodecVorbis:
|
|
return fileExtOGG, fileTypeDocument
|
|
default:
|
|
// all other cases, we return webm as document
|
|
return fileExtWebM, fileTypeDocument
|
|
}
|
|
}
|
|
|
|
func (format *MediaFormat) GetInputMedia(
|
|
filePath string,
|
|
thumbnailFilePath string,
|
|
messageCaption string,
|
|
) (gotgbot.InputMedia, error) {
|
|
if format.FileID != "" {
|
|
return format.GetInputMediaWithFileID(messageCaption)
|
|
}
|
|
|
|
_, inputMediaType := format.GetFormatInfo()
|
|
|
|
fileObj, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
fileInputMedia := gotgbot.InputFileByReader(
|
|
filepath.Base(filePath),
|
|
fileObj,
|
|
)
|
|
|
|
var thumbnailFileInputMedia gotgbot.InputFile
|
|
if thumbnailFilePath != "" {
|
|
thumbnailFileObj, err := os.Open(thumbnailFilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
thumbnailFileInputMedia = gotgbot.InputFileByReader(
|
|
filepath.Base(thumbnailFilePath),
|
|
thumbnailFileObj,
|
|
)
|
|
}
|
|
|
|
switch inputMediaType {
|
|
case fileTypeVideo:
|
|
return &gotgbot.InputMediaVideo{
|
|
Media: fileInputMedia,
|
|
Thumbnail: thumbnailFileInputMedia,
|
|
Width: format.Width,
|
|
Height: format.Height,
|
|
Duration: format.Duration,
|
|
Caption: messageCaption,
|
|
SupportsStreaming: true,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypeAudio:
|
|
return &gotgbot.InputMediaAudio{
|
|
Media: fileInputMedia,
|
|
Thumbnail: thumbnailFileInputMedia,
|
|
Duration: format.Duration,
|
|
Performer: format.Artist,
|
|
Title: format.Title,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypePhoto:
|
|
return &gotgbot.InputMediaPhoto{
|
|
Media: fileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypeDocument:
|
|
return &gotgbot.InputMediaDocument{
|
|
Media: fileInputMedia,
|
|
Thumbnail: thumbnailFileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown input type: %s", inputMediaType)
|
|
}
|
|
}
|
|
|
|
func (format *MediaFormat) GetInputMediaWithFileID(
|
|
messageCaption string,
|
|
) (gotgbot.InputMedia, error) {
|
|
_, inputMediaType := format.GetFormatInfo()
|
|
fileInputMedia := gotgbot.InputFileByID(format.FileID)
|
|
switch inputMediaType {
|
|
case fileTypeVideo:
|
|
return &gotgbot.InputMediaVideo{
|
|
Media: fileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypeAudio:
|
|
return &gotgbot.InputMediaAudio{
|
|
Media: fileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypePhoto:
|
|
return &gotgbot.InputMediaPhoto{
|
|
Media: fileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
case fileTypeDocument:
|
|
return &gotgbot.InputMediaDocument{
|
|
Media: fileInputMedia,
|
|
Caption: messageCaption,
|
|
ParseMode: gotgbot.ParseModeHTML,
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown input type: %s", inputMediaType)
|
|
}
|
|
}
|
|
|
|
func (format *MediaFormat) GetFileName() string {
|
|
extension, _ := format.GetFormatInfo()
|
|
if format.Type == enums.MediaTypeAudio && format.Title != "" && format.Artist != "" {
|
|
artist := strings.ReplaceAll(format.Artist, "/", " ")
|
|
title := strings.ReplaceAll(format.Title, "/", " ")
|
|
return fmt.Sprintf("%s - %s.%s", artist, title, extension)
|
|
}
|
|
name := uuid.New().String()
|
|
name = strings.ReplaceAll(name, "-", "")
|
|
return fmt.Sprintf("%s.%s", name, extension)
|
|
}
|
|
|
|
func (media *Media) HasVideo() bool {
|
|
for _, format := range media.Formats {
|
|
if format.Type == enums.MediaTypeVideo {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (media *Media) HasAudio() bool {
|
|
for _, format := range media.Formats {
|
|
if format.Type == enums.MediaTypeAudio {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (media *Media) HasPhoto() bool {
|
|
for _, format := range media.Formats {
|
|
if format.Type == enums.MediaTypePhoto {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (media *Media) SupportsAudio() bool {
|
|
for _, format := range media.Formats {
|
|
if format.AudioCodec != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (media *Media) SupportsAudioFromVideo() bool {
|
|
return !media.HasAudio() && media.HasVideo() && media.SupportsAudio()
|
|
}
|