Complete refactoring for rktplayer, in order to make it play on DLNA renderers and also use UpNP media server resources.

This commit is contained in:
2026-08-08 14:29:01 +02:00
parent 186b3bb8d7
commit f5fdc38e67
69 changed files with 5953 additions and 1647 deletions
+12
View File
@@ -0,0 +1,12 @@
#lang racket/base
(require racket/class)
(provide booklet-provider%)
(define booklet-provider%
(class object%
(abstract
has-booklet?
booklet-file)
(super-new)))
+13
View File
@@ -0,0 +1,13 @@
#lang racket/base
(require racket/class)
(provide image-provider%)
(define image-provider%
(class object%
(abstract
has-image?
image->file
image->mimetype)
(super-new)))
+52
View File
@@ -0,0 +1,52 @@
#lang racket/base
(require racket/class
"media-item.rkt")
(provide media-container%)
;; A media container contains media-item% instances. An item can itself be
;; another media-container%, or it can be a track<%>.
(define media-container%
(class media-item%
(init [id #f])
(super-new
[id id]
[kind 'container])
(abstract
get-title
get-items
get-track-reliver)))
(module+ test
(require rackunit)
(define track-reliver
(lambda (track-factory-id track-relive-info)
(list track-factory-id track-relive-info)))
(define test-container%
(class media-container%
(super-new [id 'container-id])
(define/override (get-title)
"Container")
(define/override (get-items)
'())
(define/override (get-track-reliver)
track-reliver)))
(define container
(new test-container%))
(check-equal? (send container get-id) 'container-id)
(check-equal? (send container get-kind) 'container)
(check-eq? (send container get-container) container)
(check-false (send container get-track))
(check-equal? (send container get-title) "Container")
(check-equal? (send container get-items) '())
(check-eq? (send container get-track-reliver) track-reliver))
+59
View File
@@ -0,0 +1,59 @@
#lang racket/base
(require racket/class)
(provide media-item%)
;; Common base class for entries returned by a media container.
(define media-item%
(class object%
(init-field
kind
[id #f])
(unless (memq kind '(container track))
(raise-arguments-error
'media-item%
"invalid media item kind"
"expected" '(container track)
"kind" kind))
(define/public (get-id)
id)
(define/public (get-kind)
kind)
(define/public (get-container)
(and (eq? kind 'container)
this))
(define/public (get-track)
(and (eq? kind 'track)
this))
(super-new)))
(module+ test
(require rackunit)
(define container
(new media-item%
[id 'container-id]
[kind 'container]))
(define track
(new media-item%
[id 'track-id]
[kind 'track]))
(check-eq? (send container get-container) container)
(check-false (send container get-track))
(check-eq? (send track get-track) track)
(check-false (send track get-container))
(check-equal? (send container get-id) 'container-id)
(check-equal? (send track get-kind) 'track)
(check-exn
exn:fail:contract?
(lambda ()
(new media-item% [kind 'unknown]))))
+51
View File
@@ -0,0 +1,51 @@
#lang racket/base
(require racket/class
"../library-cfg.rkt"
"../../misc/utils.rkt")
(provide media-library%)
(define media-library%
(class object%
(init-field
cfg)
(check/c media-library%
cfg
(is-a?/c library-cfg%))
(define cfg-revision
-1)
(define root-container
#f)
(define/private (reset-if-needed!)
(let ((current-revision (send cfg get-revision)))
(unless (= cfg-revision current-revision)
(set! cfg-revision current-revision)
(set! root-container #f))))
(define/public (get-cfg)
cfg)
(define/public (get-id)
(send cfg get-id))
(define/public (get-kind)
(send cfg get-kind))
(define/public (get-kind-version)
(send cfg get-kind-version))
(define/public (get-root-container)
(reset-if-needed!)
(when (eq? root-container #f)
(set! root-container
(send this make-root-container)))
root-container)
(abstract make-root-container)
(super-new)))
+97
View File
@@ -0,0 +1,97 @@
#lang racket/base
(require net/url
racket/class
racket/string
"../../misc/utils.rkt")
(provide media-resource%
media-resource-file%)
(define media-resource%
(class object%
(init-field
uri
mime-type
protocol-info
seekable?)
(check/c* media-resource%
(uri string?)
(mime-type (or/c #f string?))
(protocol-info (or/c #f string?))
(seekable? boolean?))
(define/public (get-uri)
uri)
(define/public (get-mime-type)
mime-type)
(define/public (get-protocol-info)
protocol-info)
(define/public (is-seekable?)
seekable?)
(define/public (get-file)
#f)
(super-new)))
(define media-resource-file%
(class media-resource%
(init
file
mime-type
[seekable? #t])
(check/c media-resource-file%
file
(or/c path? string?))
(define resource-file
(normal-case-path
(path->complete-path file)))
(define/override (get-file)
resource-file)
(super-new
[uri
(url->string
(path->url resource-file))]
[mime-type mime-type]
[protocol-info
(and mime-type
(format "file:*:~a:*" mime-type))]
[seekable? seekable?])))
(module+ test
(require rackunit)
(define resource
(new media-resource-file%
[file
(build-path
(find-system-path 'temp-dir)
"track.flac")]
[mime-type "audio/flac"]))
(check-true
(is-a? resource media-resource%))
(check-true
(is-a? resource media-resource-file%))
(check-true
(path? (send resource get-file)))
(check-true
(string-prefix? (send resource get-uri)
"file:"))
(check-equal?
(send resource get-mime-type)
"audio/flac")
(check-equal?
(send resource get-protocol-info)
"file:*:audio/flac:*")
(check-true
(send resource is-seekable?)))
+10
View File
@@ -0,0 +1,10 @@
#lang racket/base
(require racket/class)
(provide tag-data-provider%)
(define tag-data-provider%
(class object%
(abstract get-tag-data)
(super-new)))
+205
View File
@@ -0,0 +1,205 @@
#lang racket/base
(require racket/class
"booklet-provider.rkt"
"image-provider.rkt"
"media-item.rkt"
"media-resource.rkt"
"tag-data-provider.rkt"
"../track-tag-data.rkt"
"../../misc/utils.rkt")
(provide track<%>
track%)
(define track<%>
(interface ((class->interface media-item%))
get-title
get-artist
get-album
get-number
get-length
get-resource
get-music-library-factory-id
get-track-factory-id
get-track-relive-info
has-image?
image->file
image->mimetype
has-booklet?
booklet-file
track<
->log))
(define next-track-id
0)
(define (new-track-id)
(set! next-track-id (+ next-track-id 1))
(when (> next-track-id 10000000)
(set! next-track-id 1))
next-track-id)
(define track%
(class* media-item% (track<%>)
(init
tag-data-provider
image-provider
booklet-provider
resource
[id #f]
[music-library-factory-id #f]
[track-factory-id #f]
[track-relive-info #f])
(check/c* track%
(tag-data-provider
(is-a?/c tag-data-provider%))
(image-provider
(is-a?/c image-provider%))
(booklet-provider
(is-a?/c booklet-provider%))
(resource
(is-a?/c media-resource%)))
(define the-tag-data-provider
tag-data-provider)
(define the-image-provider
image-provider)
(define the-booklet-provider
booklet-provider)
(define the-resource
resource)
(define the-music-library-factory-id
music-library-factory-id)
(define the-track-factory-id
track-factory-id)
(define the-track-relive-info
track-relive-info)
(define/private (get-tag-data)
(send the-tag-data-provider get-tag-data))
(define/public (get-title)
(track-tag-data-title (get-tag-data)))
(define/public (get-artist)
(track-tag-data-artist (get-tag-data)))
(define/public (get-album)
(track-tag-data-album (get-tag-data)))
(define/public (get-number)
(track-tag-data-number (get-tag-data)))
(define/public (get-length)
(track-tag-data-length (get-tag-data)))
(define/public (get-resource)
the-resource)
(define/public (get-music-library-factory-id)
the-music-library-factory-id)
(define/public (get-track-factory-id)
the-track-factory-id)
(define/public (get-track-relive-info)
the-track-relive-info)
(define/public (has-image?)
(send the-image-provider has-image?))
(define/public (image->file target-file)
(send the-image-provider image->file target-file))
(define/public (image->mimetype)
(send the-image-provider image->mimetype))
(define/public (has-booklet?)
(send the-booklet-provider has-booklet?))
(define/public (booklet-file)
(send the-booklet-provider booklet-file))
(define/public (track< other-track)
(if (string-ci<? (send this get-album)
(send other-track get-album))
#t
(and (string-ci=? (send this get-album)
(send other-track get-album))
(< (send this get-number)
(send other-track get-number)))))
(define/public (->log)
(info-rktplayer "~a - ~a - ~a - ~a"
(send this get-number)
(send this get-title)
(send this get-album)
(send this get-length)))
(super-new
[id (if id id (new-track-id))]
[kind 'track])))
(module+ test
(require rackunit)
(define test-tag-data-provider%
(class tag-data-provider%
(define/override (get-tag-data)
(track-tag-data
"Title"
"Artist"
"Album"
2
120))
(super-new)))
(define test-image-provider%
(class image-provider%
(define/override (has-image?) #f)
(define/override (image->file target-file) #f)
(define/override (image->mimetype) 'no-mimetype)
(super-new)))
(define test-booklet-provider%
(class booklet-provider%
(define/override (has-booklet?) #f)
(define/override (booklet-file) #f)
(super-new)))
(define resource
(new media-resource%
[uri "https://example.com/track.flac"]
[mime-type "audio/flac"]
[protocol-info "http-get:*:audio/flac:*"]
[seekable? #t]))
(define track
(new track%
[resource resource]
[tag-data-provider
(new test-tag-data-provider%)]
[image-provider
(new test-image-provider%)]
[booklet-provider
(new test-booklet-provider%)]))
(check-true (is-a? track track%))
(check-true (is-a? track track<%>))
(check-equal? (send track get-kind) 'track)
(check-equal? (send track get-title) "Title")
(check-equal? (send track get-artist) "Artist")
(check-equal? (send track get-album) "Album")
(check-equal? (send track get-number) 2)
(check-equal? (send track get-length) 120)
(check-eq? (send track get-resource) resource)
(check-false (send track has-image?))
(check-false (send track has-booklet?)))
+149
View File
@@ -0,0 +1,149 @@
#lang racket/base
(require racket/class
"library-cfg.rkt"
"library-item.rkt"
"../misc/utils.rkt")
(provide libraries-config%
(all-from-out "library-cfg.rkt")
(all-from-out "library-item.rkt"))
(define libraries-config%
(class object%
(init-field settings)
(define items
#f)
(define revisions
(make-hash))
(define cfg
(send settings clone 'settings))
(define/private (sorted-items value)
(sort value
(lambda (a b)
(string<? (library-item-name a)
(library-item-name b)))))
(define/private (get-items)
(when (eq? items #f)
(let ((stored (send cfg get 'libraries '())))
(dbg-rktplayer "libraries from ini: ~a" stored)
(set! items
(sorted-items
(map store->library-item stored)))))
items)
(define/private (store-items! value)
(let ((new-items (sorted-items value)))
(send cfg
set!
'libraries
(map library-item->store new-items))
(set! items new-items)))
(define/private (increment-revision! id)
(hash-update! revisions id add1 0))
(define/public (libraries)
(map (lambda (item)
(new library-cfg%
[library-cfg-id (library-item-id item)]
[libraries-config this]))
(get-items)))
(define/public (count)
(length (get-items)))
(define/public (library-id idx)
(let ((all-items (get-items)))
(if (and (>= idx 0)
(< idx (length all-items)))
(library-item-id (list-ref all-items idx))
#f)))
(define/public (get-item id)
(check/c libraries-config% get-item id symbol?)
(findf (lambda (item)
(eq? (library-item-id item) id))
(get-items)))
(define/public (get-item-revision id)
(check/c libraries-config% get-item-revision id symbol?)
(hash-ref revisions id 0))
(define/public (get-library id)
(check/c libraries-config% get-library id symbol?)
(and (send this get-item id)
(new library-cfg%
[library-cfg-id id]
[libraries-config this])))
(define/public (remove-library id)
(check/c libraries-config% remove-library id symbol?)
(store-items!
(filter (lambda (item)
(not (eq? (library-item-id item) id)))
(get-items)))
(increment-revision! id))
(define/public (add-library item)
(check/c libraries-config% add-library item library-item?)
(let ((id (library-item-id item)))
(when (send this get-item id)
(raise-arguments-error
'libraries-config%:add-library
"a library with this id already exists"
"id" id))
(store-items! (cons item (get-items)))
(increment-revision! id)
id))
(define/public (update-item! item)
(check/c libraries-config% update-item! item library-item?)
(let* ((id (library-item-id item))
(current-item (send this get-item id)))
(unless current-item
(raise-arguments-error
'libraries-config%:update-item!
"library does not exist"
"id" id))
(unless (and (eq? (library-item-kind current-item)
(library-item-kind item))
(= (library-item-kind-version current-item)
(library-item-kind-version item)))
(raise-arguments-error
'libraries-config%:update-item!
"library kind and kind-version cannot be changed"
"id" id))
(store-items!
(map (lambda (existing)
(if (eq? (library-item-id existing) id)
item
existing))
(get-items)))
(increment-revision! id)
(void)))
(define/public (current-library)
(let ((item
(findf library-item-current
(get-items))))
(if item
(send this get-library
(library-item-id item))
(if (null? (get-items))
#f
(send this get-library
(library-item-id
(car (get-items))))))))
(super-new)))
+82
View File
@@ -0,0 +1,82 @@
#lang racket/base
(require racket/class
"base/media-container.rkt"
"base/media-library.rkt"
"../misc/utils.rkt")
(provide library-browser%)
(define library-browser%
(class object%
(init-field media-library)
(check/c library-browser%
media-library
(is-a?/c media-library%))
(define current-container
#f)
(define parent-containers
'())
(define cfg-revision
-1)
(define/private (reset-if-needed!)
(let ((current-revision
(send (send media-library get-cfg)
get-revision)))
(unless (= cfg-revision current-revision)
(set! cfg-revision current-revision)
(send this reset!))))
(define/public (get-media-library)
media-library)
(define/public (get-current-container)
(reset-if-needed!)
current-container)
(define/public (get-items)
(send (send this get-current-container)
get-items))
(define/public (can-go-up?)
(reset-if-needed!)
(not (null? parent-containers)))
(define/public (open-container! container)
(check/c library-browser% open-container!
container
(is-a?/c media-container%))
(reset-if-needed!)
(set! parent-containers
(cons current-container
parent-containers))
(set! current-container container)
(void))
(define/public (go-up!)
(reset-if-needed!)
(unless (null? parent-containers)
(set! current-container
(car parent-containers))
(set! parent-containers
(cdr parent-containers)))
(void))
(define/public (reset!)
(set! cfg-revision
(send (send media-library get-cfg)
get-revision))
(set! current-container
(send media-library get-root-container))
(set! parent-containers '())
(void))
(super-new)
(send this reset!)))
+71
View File
@@ -0,0 +1,71 @@
#lang racket/base
(require racket/class
"library-item.rkt"
"../misc/utils.rkt")
(provide library-cfg%)
(define library-cfg%
(class object%
(init-field
library-cfg-id
libraries-config)
(check/c* library-cfg%
(library-cfg-id symbol?)
(libraries-config object?))
(define/private (get-item)
(let ((item (send libraries-config
get-item
library-cfg-id)))
(unless item
(raise-arguments-error
'library-cfg%
"library configuration no longer exists"
"library-cfg-id" library-cfg-id))
item))
(define/public (get-id)
library-cfg-id)
(define/public (get-name)
(library-item-name (get-item)))
(define/public (get-kind)
(library-item-kind (get-item)))
(define/public (get-kind-version)
(library-item-kind-version (get-item)))
(define/public (get-root)
(library-item-root (get-item)))
(define/public (get-host)
(library-item-host (get-item)))
(define/public (get-item-limit)
(library-item-item-limit (get-item)))
(define/public (is-current?)
(library-item-current (get-item)))
(define/public (get-current)
(library-item-current (get-item)))
(define/public (get-revision)
(send libraries-config
get-item-revision
library-cfg-id))
(define/public (set-current! value)
(check/c library-cfg% set-current! value boolean?)
(send libraries-config
update-item!
(struct-copy library-item
(get-item)
[current value])))
(super-new)))
+108
View File
@@ -0,0 +1,108 @@
#lang racket/base
(require racket/class
"libraries-config.rkt"
"base/media-library.rkt"
"../misc/utils.rkt")
(provide library-factory%
get-library-factory
set-library-factory!)
(define current-library-factory
#f)
(define (get-library-factory)
(unless current-library-factory
(raise-arguments-error
'get-library-factory
"no library factory has been configured"))
current-library-factory)
(define (set-library-factory! factory)
(check/c set-library-factory!
factory
(is-a?/c library-factory%))
(set! current-library-factory factory)
(void))
(define library-factory%
(class object%
(init-field libraries-config)
(check/c library-factory%
libraries-config
(is-a?/c libraries-config%))
(define makers
(make-hash))
(define libraries
(make-hash))
(define/public (get-libraries-config)
libraries-config)
(define/public (register-library-maker! kind version maker)
(check/c* (library-factory% register-library-maker!)
(kind symbol?)
(version exact-positive-integer?)
(maker (-> (is-a?/c library-cfg%) any/c)))
(let ((maker-key (cons kind version)))
(when (hash-has-key? makers maker-key)
(raise-arguments-error
'library-factory%:register-library-maker!
"a library maker is already registered"
"kind" kind
"version" version))
(hash-set! makers maker-key maker)
(void)))
(define/public (get-library library-id kind version)
(check/c* (library-factory% get-library)
(library-id symbol?)
(kind symbol?)
(version exact-positive-integer?))
(hash-ref!
libraries
library-id
(lambda ()
(let ((cfg (send libraries-config
get-library
library-id)))
(unless cfg
(raise-arguments-error
'library-factory%:get-library
"library configuration does not exist"
"library-id" library-id))
(unless (and (eq? kind (send cfg get-kind))
(= version (send cfg get-kind-version)))
(raise-arguments-error
'library-factory%:get-library
"library kind or version does not match its configuration"
"library-id" library-id
"kind" kind
"version" version))
(let* ((maker-key (cons kind version))
(maker
(hash-ref
makers
maker-key
(lambda ()
(raise-arguments-error
'library-factory%:get-library
"no library maker is registered"
"kind" kind
"version" version))))
(library (maker cfg)))
(check/c library-factory% get-library
library
(is-a?/c media-library%))
library)))))
(super-new)))
+66
View File
@@ -0,0 +1,66 @@
#lang racket/base
(require racket/class
"mc-filesystem.rkt"
"library-factory.rkt"
"base/media-library.rkt"
"track-filesystem.rkt"
"../misc/utils.rkt")
(provide library-filesystem%
register-library-filesystem!)
(define library-filesystem-kind
'filesystem)
(define library-filesystem-version
1)
(define (register-library-filesystem! factory)
(check/c register-library-filesystem!
factory
(is-a?/c library-factory%))
(send factory
register-library-maker!
library-filesystem-kind
library-filesystem-version
(lambda (cfg)
(new library-filesystem%
[cfg cfg]))))
(define library-filesystem%
(class media-library%
(init
cfg)
(define/override (make-root-container)
(new mc-filesystem%
[library this]
[relative-path '()]))
(define/public (make-container relative-path)
(new mc-filesystem%
[library this]
[relative-path relative-path]))
(define/public (make-track relative-path)
(new track-filesystem%
[library this]
[relative-path relative-path]))
(define/public (resolve-path relative-path)
(check/c library-filesystem% resolve-path
relative-path
list?)
(let ((root
(normal-case-path
(send (send this get-cfg)
get-root))))
(if (null? relative-path)
root
(apply build-path root relative-path))))
(super-new
[cfg cfg])))
+74
View File
@@ -0,0 +1,74 @@
#lang racket/base
(require "../misc/utils.rkt")
(provide
(struct-out library-item)
library-item->store
store->library-item)
(define library-item-store-version
1)
(struct library-item
(id
name
kind
kind-version
root
host
item-limit
current)
#:transparent
#:guard
(lambda (id name kind kind-version root host item-limit current type-name)
(check/c* library-item
(id symbol?)
(name string?)
(kind (or/c 'filesystem 'media-server))
(kind-version exact-positive-integer?)
(root (or/c path? string?))
(host (or/c #f string?))
(item-limit exact-positive-integer?)
(current boolean?))
(values id
name
kind
kind-version
root
host
item-limit
current)))
(define (library-item->store item)
(check/c library-item->store item library-item?)
(let ((root (library-item-root item)))
(hash
'version library-item-store-version
'id (library-item-id item)
'name (library-item-name item)
'kind (library-item-kind item)
'kind-version (library-item-kind-version item)
'root (if (path? root) (path->string root) root)
'host (library-item-host item)
'item-limit (library-item-item-limit item)
'current (library-item-current item))))
(define (store->library-item stored)
(check/c store->library-item stored hash?)
(let ((version (hash-ref stored 'version #f)))
(check/c store->library-item
version
(=/c library-item-store-version))
(library-item
(hash-ref stored 'id)
(hash-ref stored 'name)
(hash-ref stored 'kind)
(hash-ref stored 'kind-version)
(hash-ref stored 'root)
(hash-ref stored 'host)
(hash-ref stored 'item-limit 100)
(hash-ref stored 'current))))
+219
View File
@@ -0,0 +1,219 @@
#lang racket/base
(require racket/class
racket/list
racket/match
racket/string
(prefix-in upnp: racket-upnp)
"library-factory.rkt"
"mc-media-server.rkt"
"base/media-library.rkt"
"track-media-server.rkt"
"../misc/utils.rkt")
(provide library-media-server%
register-library-media-server!)
(define library-media-server-kind
'media-server)
(define library-media-server-version
1)
(define (register-library-media-server! factory)
(check/c register-library-media-server!
factory
(is-a?/c library-factory%))
(send factory
register-library-maker!
library-media-server-kind
library-media-server-version
(lambda (cfg)
(new library-media-server%
[cfg cfg]))))
(define library-media-server%
(class media-library%
(init
cfg)
(define server
#f)
(define server-cfg-revision
-1)
(define/private (server-selector)
(or (send (send this get-cfg)
get-host)
(send (send this get-cfg)
get-name)))
(define/private (server-matches? candidate selector)
(let ((name
(upnp:media-server-name candidate))
(address
(upnp:media-server-address candidate))
(udn
(upnp:upnp-device-udn candidate)))
(or
(and udn
(string-ci=? udn selector))
(and name
(string-ci=? name selector))
(and address
(string-ci=? address selector))
(and name
(string-contains?
(string-downcase name)
(string-downcase selector))))))
(define/private (get-server)
(let ((cfg-revision
(send (send this get-cfg)
get-revision)))
(unless (= cfg-revision
server-cfg-revision)
(set! server #f)
(set! server-cfg-revision
cfg-revision))
(unless server
(let* ((selector (server-selector))
(found
(findf
(lambda (candidate)
(server-matches?
candidate
selector))
(upnp:query-media-servers))))
(unless found
(raise-arguments-error
'library-media-server%
"configured media server was not found"
"selector" selector))
(set! server found)))
server))
(define/private (root-container-id)
(format "~a"
(send (send this get-cfg)
get-root)))
(define/private (browse-page container-id start count)
(with-handlers
(((lambda (exception)
(and
(upnp:exn:fail:upnp? exception)
(equal?
(format "~a"
(upnp:exn:fail:upnp-code
exception))
"701")
(equal? container-id
(root-container-id))
(not (string=? container-id
"0"))))
(lambda (exception)
(warn-rktplayer
(string-append
"Configured UPnP media-server root ~a "
"does not exist; browsing root 0")
container-id)
(upnp:media-server-browse
(get-server)
"0"
#:start start
#:count count))))
(upnp:media-server-browse
(get-server)
container-id
#:start start
#:count count)))
(define/public (browse-container container-id)
(check/c library-media-server% browse-container
container-id
string?)
(browse-page
container-id
0
(send (send this get-cfg)
get-item-limit)))
(define/private (find-entry parent-id entry-id)
(let ((page-size
(send (send this get-cfg)
get-item-limit)))
(let loop ((start 0))
(let* ((entries
(browse-page parent-id
start
page-size))
(entry
(findf
(lambda (candidate)
(equal?
(upnp:media-entry-id candidate)
entry-id))
entries)))
(cond
(entry entry)
((< (length entries)
page-size)
#f)
(else
(loop (+ start
page-size))))))))
(define/override (make-root-container)
(new mc-media-server%
[library this]
[container-id
(root-container-id)]
[title
(send (send this get-cfg)
get-name)]))
(define/public (make-container entry)
(check/c library-media-server% make-container
entry
upnp:media-container?)
(new mc-media-server%
[library this]
[container-id
(upnp:media-entry-id entry)]
[title
(upnp:media-entry-title entry)]))
(define/public (make-track entry)
(check/c library-media-server% make-track
entry
upnp:media-item?)
(new track-media-server%
[library this]
[entry entry]))
(define/public (relive-track track-factory-id
track-relive-info)
(case track-factory-id
((media-server-item)
(match track-relive-info
((list (? string? parent-id)
(? string? entry-id))
(let ((entry
(find-entry parent-id
entry-id)))
(and entry
(upnp:media-item? entry)
(send this
make-track
entry))))
(else #f)))
(else #f)))
(super-new
[cfg cfg])))
+7
View File
@@ -0,0 +1,7 @@
#lang racket/base
(provide (struct-out library-ref))
(struct library-ref
(library-id kind version)
#:prefab)
+82
View File
@@ -0,0 +1,82 @@
#lang racket/base
(require racket/class
racket-audio
racket/list
racket/path
racket/string
"base/media-container.rkt"
"../misc/utils.rkt")
(provide mc-filesystem%)
(define mc-filesystem%
(class media-container%
(init-field
library
[relative-path '()])
(check/c mc-filesystem% relative-path list?)
(define/private (full-path)
(send library resolve-path relative-path))
(define/private (music-file-name? path)
(let ((file-name
(string-downcase (path->string path))))
(for/or ((extension
(in-list (audio-known-exts?))))
(string-suffix?
file-name
(string-append "." extension)))))
(define/private (item-kind path)
(cond
((directory-exists? path)
(let ((name
(path->string
(file-name-from-path path))))
(and (not (string-prefix? name "."))
'container)))
((music-file-name? path) 'track)
(else #f)))
(define/override (get-title)
(if (null? relative-path)
(send (send library get-cfg) get-name)
(path->string (last relative-path))))
(define/override (get-items)
(let ((path (full-path)))
(if (directory-exists? path)
(for*/list ((entry (in-list
(sort (directory-list path)
path<?)))
(entry-path
(in-value (build-path path entry)))
(kind
(in-value (item-kind entry-path)))
#:when kind)
(let ((entry-relative-path
(append relative-path (list entry))))
(if (eq? kind 'container)
(send library
make-container
entry-relative-path)
(send library
make-track
entry-relative-path))))
'())))
(define/override (get-track-reliver)
(lambda (track-factory-id track-relive-info)
(case track-factory-id
((file)
(send library
make-track
track-relive-info))
(else #f))))
(super-new
[id (cons (send (send library get-cfg) get-id)
relative-path)])))
+86
View File
@@ -0,0 +1,86 @@
#lang racket/base
(require racket/class
racket/list
racket/string
(prefix-in upnp: racket-upnp)
"base/media-container.rkt"
"../misc/utils.rkt")
(provide mc-media-server%)
(define mc-media-server%
(class media-container%
(init-field
library
container-id
title)
(check/c* mc-media-server%
(library object?)
(container-id string?)
(title string?))
(define/private (audio-item? entry)
(and
(upnp:media-item? entry)
(string? (upnp:media-entry-id entry))
(string? (upnp:media-entry-parent-id entry))
(not
(null?
(upnp:media-item-resources entry)))
(or
(let ((class
(upnp:media-entry-class entry)))
(and class
(string-prefix?
class
"object.item.audioItem")))
(for/or
((resource
(in-list
(upnp:media-item-resources entry))))
(let ((content-type
(upnp:media-resource-content-type
resource)))
(and content-type
(string-prefix?
content-type
"audio/")))))))
(define/override (get-title)
title)
(define/override (get-items)
(filter-map
(lambda (entry)
(cond
((and (upnp:media-container? entry)
(string?
(upnp:media-entry-id entry)))
(send library
make-container
entry))
((audio-item? entry)
(send library
make-track
entry))
(else #f)))
(send library
browse-container
container-id)))
(define/override (get-track-reliver)
(lambda (track-factory-id
track-relive-info)
(send library
relive-track
track-factory-id
track-relive-info)))
(super-new
[id
(cons
(send (send library get-cfg)
get-id)
container-id)])))
+159
View File
@@ -0,0 +1,159 @@
#lang racket
(require racket-audio
"base/booklet-provider.rkt"
"base/image-provider.rkt"
"base/tag-data-provider.rkt"
"track-tag-data.rkt")
(provide tag-source-filesystem%
tag-data-provider-filesystem%
image-provider-filesystem%
booklet-provider-filesystem%)
(define tag-source-filesystem%
(class object%
(init-field file)
(define tags
#f)
(define loaded?
#f)
(define/private (read-tags)
(if (and file (file-exists? file))
(let* ((source-file
(if (path? file)
(path->string file)
file))
(source-tags (id3-tags source-file)))
(if (tags-valid? source-tags)
source-tags
(let ((temporary-file
(make-temporary-file
"rktplayer-~a"
#:copy-from source-file)))
(let ((temporary-tags
(id3-tags temporary-file)))
(delete-file temporary-file)
temporary-tags))))
#f))
(define/public (get-tags)
(unless loaded?
(set! tags (read-tags))
(set! loaded? #t))
tags)
(super-new)))
(define tag-data-provider-filesystem%
(class tag-data-provider%
(init-field
tag-source
[fallback-data (track-tag-data "" "" "" 0 0)])
(define tag-data
#f)
(define/override (get-tag-data)
(unless tag-data
(let ((tags (send tag-source get-tags)))
(set! tag-data
(if (and tags (tags-valid? tags))
(track-tag-data
(tags-title tags)
(tags-artist tags)
(tags-album tags)
(tags-track tags)
(tags-length tags))
fallback-data))))
tag-data)
(super-new)))
(define image-provider-filesystem%
(class image-provider%
(init-field file tag-source)
(define image-names
'("cover.jpg" "cover.png" "folder.jpg" "folder.png"))
(define/private (image-from-directory)
(and file
(let ((directory (path-only file)))
(for/first ((image-name (in-list image-names))
#:when
(file-exists?
(build-path directory image-name)))
(build-path directory image-name)))))
(define/override (has-image?)
(let ((tags (send tag-source get-tags)))
(or (and tags
(tags-valid? tags)
(not (eq? (tags-picture->ext tags) #f)))
(not (eq? (image-from-directory) #f)))))
(define/override (image->file target-file)
(let* ((target (format "~a" target-file))
(tags (send tag-source get-tags))
(picture-extension
(and tags
(tags-valid? tags)
(tags-picture->ext tags))))
(if picture-extension
(let ((stored-file
(string-append
target
"."
(symbol->string picture-extension))))
(and (tags-picture->file tags stored-file)
stored-file))
(let ((source-file (image-from-directory)))
(and source-file
(let ((stored-file
(string-append
target
(bytes->string/utf-8
(path-get-extension source-file)))))
(copy-file source-file
stored-file
#:exists-ok? #t)
(format "~a" stored-file)))))))
(define/override (image->mimetype)
(let ((tags (send tag-source get-tags)))
(if (and tags
(tags-valid? tags)
(not (eq? (tags-picture->ext tags) #f)))
(tags-picture->mimetype tags)
(let ((source-file (image-from-directory)))
(if source-file
(case (string->symbol
(string-downcase
(bytes->string/utf-8
(path-get-extension source-file))))
((|.jpg| |.jpeg|) "image/jpeg")
((|.png|) "image/png")
(else 'no-mimetype))
'no-mimetype)))))
(super-new)))
(define booklet-provider-filesystem%
(class booklet-provider%
(init-field file)
(define/override (booklet-file)
(and file
(build-path (path-only file)
"booklet.pdf")))
(define/override (has-booklet?)
(let ((booklet (send this booklet-file)))
(and booklet
(file-exists? booklet))))
(super-new)))
+116
View File
@@ -0,0 +1,116 @@
#lang racket/base
(require racket-mimetypes/mimetypes
racket/class
racket/file
racket/path
"library-ref.rkt"
"base/media-resource.rkt"
"track-filesystem-providers.rkt"
"track-tag-data.rkt"
"base/track.rkt"
"../misc/utils.rkt")
(provide track-filesystem%)
(define track-filesystem%
(class track%
(init-field library relative-path)
(check/c track-filesystem%
relative-path
list?)
(define file
(send library resolve-path relative-path))
(define mime-type
(mimetype-for-ext
file
#:default "application/octet-stream"))
(define tag-source
(new tag-source-filesystem%
[file file]))
(super-new
[resource
(new media-resource-file%
[file file]
[mime-type mime-type])]
[music-library-factory-id
(let ((cfg (send library get-cfg)))
(library-ref
(send cfg get-id)
(send cfg get-kind)
(send cfg get-kind-version)))]
[track-factory-id 'file]
[track-relive-info relative-path]
[tag-data-provider
(new tag-data-provider-filesystem%
[tag-source tag-source]
[fallback-data
(track-tag-data
(path->string
(file-name-from-path file))
""
""
0
0)])]
[image-provider
(new image-provider-filesystem%
[file file]
[tag-source tag-source])]
[booklet-provider
(new booklet-provider-filesystem%
[file file])])))
(module+ test
(require rackunit)
(define file
(make-temporary-file "rktplayer-track-~a.mp3"))
(define cfg%
(class object%
(define/public (get-id) 'test-library)
(define/public (get-kind) 'filesystem)
(define/public (get-kind-version) 1)
(super-new)))
(define library%
(class object%
(define/public (resolve-path relative-path)
file)
(define/public (get-cfg)
(new cfg%))
(super-new)))
(dynamic-wind
void
(lambda ()
(let* ((track
(new track-filesystem%
[library (new library%)]
[relative-path
(list (file-name-from-path file))]))
(resource (send track get-resource))
(library-reference
(send track get-music-library-factory-id)))
(check-true (is-a? track track%))
(check-equal?
(send resource get-file)
(normal-case-path
(path->complete-path file)))
(check-equal? (send track get-track-factory-id) 'file)
(check-equal? (send track get-track-relive-info)
(list (file-name-from-path file)))
(check-true (is-a? resource media-resource-file%))
(check-true (send resource is-seekable?))
(check-equal? (send resource get-mime-type)
"audio/mpeg")
(check-equal? (library-ref-library-id library-reference)
'test-library)))
(lambda ()
(when (file-exists? file)
(delete-file file)))))
+191
View File
@@ -0,0 +1,191 @@
#lang racket/base
(require net/url
racket-mimetypes/mimetypes
racket/class
racket/list
racket/port
racket/string
(prefix-in upnp: racket-upnp)
"base/booklet-provider.rkt"
"base/image-provider.rkt"
"library-ref.rkt"
"base/media-resource.rkt"
"base/tag-data-provider.rkt"
"track-tag-data.rkt"
"base/track.rkt"
"../misc/utils.rkt")
(provide track-media-server%)
(define tag-data-provider-media-server%
(class tag-data-provider%
(init-field
entry
resource)
(define/override (get-tag-data)
(let ((artists
(upnp:media-item-artists entry)))
(track-tag-data
(upnp:media-entry-title entry)
(cond
((not (null? artists))
(car artists))
((upnp:media-item-creator entry)
(upnp:media-item-creator entry))
(else ""))
(or (upnp:media-item-album entry)
"")
(or
(upnp:media-item-original-track-number
entry)
0)
(or (upnp:media-resource-duration
resource)
0))))
(super-new)))
(define image-provider-media-server%
(class image-provider%
(init-field uri)
(define mime-type
(and uri
(mimetype-for-ext
(regexp-replace
#px"[?#].*$"
uri
"")
#:default
"application/octet-stream")))
(define/private (stored-file target-file)
(let ((extension
(cond
((equal? mime-type "image/jpeg") ".jpg")
((equal? mime-type "image/png") ".png")
(else ""))))
(string-append
(format "~a" target-file)
extension)))
(define/override (has-image?)
(and (string? uri)
(not (string=? uri ""))))
(define/override (image->file target-file)
(and
(send this has-image?)
(with-handlers
((exn:fail?
(lambda (exception)
(warn-rktplayer
"Could not retrieve media-server image: ~a"
(exn-message exception))
#f)))
(let ((file (stored-file target-file)))
(call/input-url
(string->url uri)
get-pure-port
(lambda (input)
(call-with-output-file
file
(lambda (output)
(copy-port input output))
#:exists 'replace)))
file))))
(define/override (image->mimetype)
(or mime-type
'no-mimetype))
(super-new)))
(define booklet-provider-media-server%
(class booklet-provider%
(define/override (has-booklet?)
#f)
(define/override (booklet-file)
#f)
(super-new)))
(define (audio-resource? resource)
(let ((content-type
(upnp:media-resource-content-type
resource)))
(and content-type
(string-prefix?
content-type
"audio/"))))
(define (resource-seekable? resource)
(let ((protocol-info
(upnp:media-resource-protocol-info
resource)))
(and protocol-info
(regexp-match?
#px"DLNA[.]ORG_OP=(?:01|10|11)"
protocol-info))))
(define track-media-server%
(class track%
(init-field
library
entry)
(check/c track-media-server%
entry
upnp:media-item?)
(define source-resource
(or
(findf
audio-resource?
(upnp:media-item-resources entry))
(car
(upnp:media-item-resources entry))))
(define resource
(new media-resource%
[uri
(upnp:media-resource-uri
source-resource)]
[mime-type
(upnp:media-resource-content-type
source-resource)]
[protocol-info
(upnp:media-resource-protocol-info
source-resource)]
[seekable?
(resource-seekable?
source-resource)]))
(super-new
[resource resource]
[music-library-factory-id
(let ((cfg (send library get-cfg)))
(library-ref
(send cfg get-id)
(send cfg get-kind)
(send cfg get-kind-version)))]
[track-factory-id
'media-server-item]
[track-relive-info
(list
(upnp:media-entry-parent-id entry)
(upnp:media-entry-id entry))]
[tag-data-provider
(new tag-data-provider-media-server%
[entry entry]
[resource source-resource])]
[image-provider
(new image-provider-media-server%
[uri
(upnp:media-item-album-art-uri
entry)])]
[booklet-provider
(new booklet-provider-media-server%)])))
+175
View File
@@ -0,0 +1,175 @@
#lang racket/base
(require racket/class
racket/match
"library-factory.rkt"
"library-ref.rkt"
"base/track.rkt"
"../misc/utils.rkt")
(provide track-store?
track-store-id
track-store-number
track-store-title
track->store
store->track)
(define track-store-version
1)
(define (track-store? stored)
(match stored
((list 'track
(== track-store-version)
_
number
title
library-reference
track-factory-id
_)
(and (exact-integer? number)
(string? title)
(library-ref? library-reference)
(symbol? track-factory-id)))
(else #f)))
(define (track-store-id stored)
(check/c track-store-id stored track-store?)
(list-ref stored 2))
(define (track-store-number stored)
(check/c track-store-number stored track-store?)
(list-ref stored 3))
(define (track-store-title stored)
(check/c track-store-title stored track-store?)
(list-ref stored 4))
(define (track->store track)
(check/c track->store track (is-a?/c track<%>))
(let ((stored
(list 'track
track-store-version
(send track get-id)
(send track get-number)
(send track get-title)
(send track get-music-library-factory-id)
(send track get-track-factory-id)
(send track get-track-relive-info))))
(check/c track->store stored track-store?)
stored))
(define (store->track stored factory)
(check/c store->track
factory
(is-a?/c library-factory%))
(and
(track-store? stored)
(with-handlers ((exn:fail?
(lambda (_)
#f)))
(let* ((library-reference (list-ref stored 5))
(library
(send factory
get-library
(library-ref-library-id library-reference)
(library-ref-kind library-reference)
(library-ref-version library-reference)))
(root-container
(send library get-root-container))
(track-reliver
(send root-container get-track-reliver))
(track
(track-reliver
(list-ref stored 6)
(list-ref stored 7))))
(and (object? track)
(is-a? track track<%>)
track)))))
(module+ test
(require rackunit
racket/file
"libraries-config.rkt"
"library-filesystem.rkt"
"library-item.rkt")
(define settings%
(class object%
(define values
(make-hash))
(define/public (clone _)
this)
(define/public (get key default)
(hash-ref values key default))
(define/public (set! key value)
(hash-set! values key value))
(super-new)))
(define root
(make-temporary-file
"rktplayer-track-store-~a"
'directory))
(define file-name
"track.mp3")
(define file
(build-path root file-name))
(dynamic-wind
(lambda ()
(call-with-output-file file void))
(lambda ()
(let* ((libraries-config
(new libraries-config%
[settings (new settings%)]))
(factory
(new library-factory%
[libraries-config libraries-config])))
(send libraries-config
add-library
(library-item
'test-library
"Test library"
'filesystem
1
root
#f
100
#t))
(register-library-filesystem! factory)
(let* ((library
(send factory
get-library
'test-library
'filesystem
1))
(track
(send library
make-track
(list (string->path file-name))))
(stored (track->store track))
(relived (store->track stored factory)))
(check-true (track-store? stored))
(check-equal? (track-store-id stored)
(send track get-id))
(check-equal? (track-store-number stored)
(send track get-number))
(check-equal? (track-store-title stored)
(send track get-title))
(check-true (is-a? relived track<%>))
(check-equal?
(normal-case-path
(send (send relived get-resource)
get-file))
(normal-case-path file)))))
(lambda ()
(delete-directory/files root))))
+7
View File
@@ -0,0 +1,7 @@
#lang racket/base
(provide (struct-out track-tag-data))
(struct track-tag-data
(title artist album number length)
#:prefab)