@@ -3,21 +3,20 @@ package ctrlsubsonic
3
3
import (
4
4
"errors"
5
5
"fmt"
6
- "io"
7
6
"log"
8
7
"net/http"
9
8
"os"
10
9
"path"
11
10
"time"
12
11
13
12
"github.com/disintegration/imaging"
13
+ "github.com/jinzhu/gorm"
14
14
15
15
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
16
16
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
17
17
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
18
18
"go.senan.xyz/gonic/server/db"
19
- "go.senan.xyz/gonic/server/encode"
20
- "go.senan.xyz/gonic/server/mime"
19
+ "go.senan.xyz/gonic/server/transcode"
21
20
)
22
21
23
22
// "raw" handlers are ones that don't always return a spec response.
@@ -26,42 +25,60 @@ import (
26
25
// b) return a non-nil spec.Response
27
26
// _but not both_
28
27
29
- func streamGetTransPref (dbc * db.DB , userID int , client string ) db.TranscodePreference {
30
- pref := db.TranscodePreference {}
31
- dbc .
28
+ func streamGetTransPref (dbc * db.DB , userID int , client string ) ( * db.TranscodePreference , error ) {
29
+ var pref db.TranscodePreference
30
+ err := dbc .
32
31
Where ("user_id=?" , userID ).
33
32
Where ("client COLLATE NOCASE IN (?)" , []string {"*" , client }).
34
33
Order ("client DESC" ). // ensure "*" is last if it's there
35
- First (& pref )
36
- return pref
34
+ First (& pref ).
35
+ Error
36
+ if err != nil {
37
+ return nil , fmt .Errorf ("search sort: %w" , err )
38
+ }
39
+ return & pref , nil
37
40
}
38
41
39
42
func streamGetTrack (dbc * db.DB , trackID int ) (* db.Track , error ) {
40
- track := db.Track {}
43
+ var track db.Track
41
44
err := dbc .
42
45
Preload ("Album" ).
43
46
First (& track , trackID ).
44
47
Error
45
- return & track , err
48
+ if err != nil {
49
+ return nil , fmt .Errorf ("search track: %w" , err )
50
+ }
51
+ return & track , nil
46
52
}
47
53
48
54
func streamGetPodcast (dbc * db.DB , podcastID int ) (* db.PodcastEpisode , error ) {
49
- podcast := db.PodcastEpisode {}
55
+ var podcast db.PodcastEpisode
50
56
err := dbc .First (& podcast , podcastID ).Error
51
- return & podcast , err
57
+ if err != nil {
58
+ return nil , fmt .Errorf ("search podcast: %w" , err )
59
+ }
60
+ return & podcast , nil
52
61
}
53
62
54
- func streamUpdateStats (dbc * db.DB , userID , albumID int ) {
55
- play := db.Play {
56
- AlbumID : albumID ,
57
- UserID : userID ,
58
- }
59
- dbc .
63
+ func streamUpdateStats (dbc * db.DB , userID , albumID int ) error {
64
+ var play db.Play
65
+ play . AlbumID = albumID
66
+ play . UserID = userID
67
+
68
+ err := dbc .
60
69
Where (play ).
61
- First (& play )
70
+ FirstOrCreate (& play ).
71
+ Error
72
+ if err != nil {
73
+ return fmt .Errorf ("first or create stats: %w" , err )
74
+ }
62
75
play .Time = time .Now () // for getAlbumList?type=recent
63
76
play .Count ++ // for getAlbumList?type=frequent
64
- dbc .Save (& play )
77
+
78
+ if err := dbc .Save (& play ).Error ; err != nil {
79
+ return fmt .Errorf ("save stats: %w" , err )
80
+ }
81
+ return nil
65
82
}
66
83
67
84
const (
@@ -219,103 +236,72 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
219
236
if err != nil {
220
237
return spec .NewError (10 , "please provide an `id` parameter" )
221
238
}
222
- var audioFile db.AudioFile
239
+ var file db.AudioFile
223
240
var audioPath string
224
241
switch id .Type {
225
242
case specid .Track :
226
243
track , err := streamGetTrack (c .DB , id .Value )
227
244
if err != nil {
228
245
return spec .NewError (70 , "track with id `%s` was not found" , id )
229
246
}
230
- audioFile = track
247
+ file = track
231
248
audioPath = path .Join (track .AbsPath ())
232
249
case specid .PodcastEpisode :
233
250
podcast , err := streamGetPodcast (c .DB , id .Value )
234
251
if err != nil {
235
252
return spec .NewError (70 , "podcast with id `%s` was not found" , id )
236
253
}
237
- audioFile = podcast
254
+ file = podcast
238
255
audioPath = path .Join (c .PodcastsPath , podcast .Path )
239
256
default :
240
257
return spec .NewError (70 , "media type of `%s` was not found" , id .Type )
241
258
}
242
259
243
260
user := r .Context ().Value (CtxUser ).(* db.User )
244
- if track , ok := audioFile .(* db.Track ); ok && track .Album != nil {
245
- defer streamUpdateStats (c .DB , user .ID , track .Album .ID )
261
+ if track , ok := file .(* db.Track ); ok && track .Album != nil {
262
+ defer func () {
263
+ if err := streamUpdateStats (c .DB , user .ID , track .Album .ID ); err != nil {
264
+ log .Printf ("error updating status: %v" , err )
265
+ }
266
+ }()
246
267
}
247
268
248
- pref := streamGetTransPref ( c . DB , user . ID , params .GetOr ("c" , "" ) )
249
- onInvalidProfile := func () error {
250
- log . Printf ( "serving raw `%s` \n " , audioFile . AudioFilename ())
251
- w . Header (). Set ( "Content-Type" , audioFile . MIME ())
252
- http . ServeFile ( w , r , audioPath )
253
- return nil
269
+ client := params .GetOr ("c" , "" )
270
+ maxBitRate := params . GetOrInt ( "maxBitRate" , 0 )
271
+
272
+ logt := func ( f string , a ... interface {}) {
273
+ log . Printf ( "requested %q (%dkbps, max %dkbps) %s" ,
274
+ file . AudioFilename (), file . AudioBitrate (), maxBitRate , fmt . Sprintf ( f , a ... ))
254
275
}
255
- onCacheHit := func (profile encode.Profile , path string ) error {
256
- log .Printf ("serving transcode `%s`: cache [%s/%dk] hit!\n " ,
257
- audioFile .AudioFilename (), profile .Format , profile .Bitrate )
258
- cacheMime , _ := mime .FromExtension (profile .Format )
259
- w .Header ().Set ("Content-Type" , cacheMime )
260
276
261
- cacheFile , err := os .Stat (path )
262
- if err != nil {
263
- return fmt .Errorf ("failed to stat cache file `%s`: %w" , path , err )
264
- }
265
- contentLength := fmt .Sprintf ("%d" , cacheFile .Size ())
266
- w .Header ().Set ("Content-Length" , contentLength )
267
- http .ServeFile (w , r , path )
268
- return nil
277
+ pref , err := streamGetTransPref (c .DB , user .ID , client )
278
+ if err != nil && ! errors .Is (err , gorm .ErrRecordNotFound ) {
279
+ return spec .NewError (0 , "couldn't find transcode preference: %v" , err )
269
280
}
270
- onCacheMiss := func (profile encode.Profile ) (io.Writer , error ) {
271
- log .Printf ("serving transcode `%s`: cache [%s/%dk] miss!\n " ,
272
- audioFile .AudioFilename (), profile .Format , profile .Bitrate )
273
- encodeMime , _ := mime .FromExtension (profile .Format )
274
- w .Header ().Set ("Content-Type" , encodeMime )
275
- return w , nil
281
+ if pref == nil {
282
+ logt ("not transcoding" )
283
+ http .ServeFile (w , r , audioPath )
284
+ return nil
276
285
}
277
- encodeOptions := encode.Options {
278
- TrackPath : audioPath ,
279
- TrackBitrate : audioFile .AudioBitrate (),
280
- CachePath : c .CachePath ,
281
- ProfileName : pref .Profile ,
282
- PreferredBitrate : params .GetOrInt ("maxBitRate" , 0 ),
283
- OnInvalidProfile : onInvalidProfile ,
284
- OnCacheHit : onCacheHit ,
285
- OnCacheMiss : onCacheMiss ,
286
+
287
+ profile , ok := transcode .UserProfiles [pref .Profile ]
288
+ if ! ok {
289
+ return spec .NewError (0 , "unknown transcode user profile %q" , pref .Profile )
286
290
}
287
- if err := encode .Encode (encodeOptions ); err != nil {
288
- log .Printf ("serving transcode `%s`: error: %v\n " , audioFile .AudioFilename (), err )
291
+ if maxBitRate > 0 && int (profile .BitRate ()) > maxBitRate {
292
+ profile = transcode .WithBitrate (profile , transcode .BitRate (maxBitRate ))
293
+ logt ("capping bitrate and transcoding / reading cache" )
294
+ } else {
295
+ logt ("transcoding / reading cache" )
289
296
}
290
- return nil
291
- }
292
297
293
- func (c * Controller ) ServeDownload (w http.ResponseWriter , r * http.Request ) * spec.Response {
294
- params := r .Context ().Value (CtxParams ).(params.Params )
295
- id , err := params .GetID ("id" )
296
- if err != nil {
297
- return spec .NewError (10 , "please provide an `id` parameter" )
298
- }
299
- var filePath string
300
- var audioFile db.AudioFile
301
- switch id .Type {
302
- case specid .Track :
303
- track , _ := streamGetTrack (c .DB , id .Value )
304
- audioFile = track
305
- filePath = track .AbsPath ()
306
- if err != nil {
307
- return spec .NewError (70 , "track with id `%s` was not found" , id )
308
- }
309
- case specid .PodcastEpisode :
310
- podcast , err := streamGetPodcast (c .DB , id .Value )
311
- audioFile = podcast
312
- filePath = path .Join (c .PodcastsPath , podcast .Path )
313
- if err != nil {
314
- return spec .NewError (70 , "podcast with id `%s` was not found" , id )
315
- }
298
+ guessedSizeBytes := transcode .GuessExpectedSize (profile , time .Duration (file .AudioLength ())* time .Second )
299
+ w .Header ().Set ("Content-Type" , profile .MIME ())
300
+ w .Header ().Set ("Content-Length" , fmt .Sprint (guessedSizeBytes ))
301
+ w .Header ().Set ("Accept-Ranges" , "none" )
302
+
303
+ if err := c .Transcoder .Transcode (r .Context (), profile , audioPath , w ); err != nil {
304
+ return spec .NewError (0 , "error transcoding %v" , err )
316
305
}
317
- log .Printf ("serving raw `%s`\n " , audioFile .AudioFilename ())
318
- w .Header ().Set ("Content-Type" , audioFile .MIME ())
319
- http .ServeFile (w , r , filePath )
320
306
return nil
321
307
}
0 commit comments