├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── api.js
├── default-config.json
├── examples
├── configuration-1fichier.json
├── configuration-icecast.json
├── configuration-repositories.json
└── configuration-tmdb.json
├── icon
├── README.md
├── icon_128.png
├── icon_32.png
├── icon_512.png
└── icon_scalable.svg
├── lib
├── class
│ ├── UPNP_attributes_for_class_items.js
│ ├── object.container.album.js
│ ├── object.container.album.musicAlbum.js
│ ├── object.container.album.photoAlbum.dateTaken.js
│ ├── object.container.album.photoAlbum.js
│ ├── object.container.album.videoAlbum.js
│ ├── object.container.audioContainer.js
│ ├── object.container.genre.js
│ ├── object.container.genre.musicGenre.js
│ ├── object.container.genre.videoGenre.js
│ ├── object.container.js
│ ├── object.container.person.js
│ ├── object.container.person.movieActor.js
│ ├── object.container.person.musicArtist.js
│ ├── object.container.playlistContainer.js
│ ├── object.container.tvShows.js
│ ├── object.item.audioItem.audioBook.js
│ ├── object.item.audioItem.audioBroadcast.js
│ ├── object.item.audioItem.js
│ ├── object.item.audioItem.musicTrack.js
│ ├── object.item.imageItem.js
│ ├── object.item.imageItem.photo.js
│ ├── object.item.js
│ ├── object.item.textItem.js
│ ├── object.item.videoItem.js
│ ├── object.item.videoItem.movie.js
│ ├── object.item.videoItem.musicVideoClip.js
│ ├── object.item.videoItem.videoBroadcast.js
│ ├── object.js
│ └── object.res.js
├── connectionManagerService.js
├── contentDirectoryService.js
├── contentHandlers
│ ├── abstract_metas.js
│ ├── allo.js
│ ├── audio_musicmetadata.js
│ ├── contentHandler.js
│ ├── exif.js
│ ├── ffprobe.js
│ ├── metas.images.js
│ ├── metas.json.js
│ ├── omdb.js
│ ├── srt.js
│ ├── tmdb.js
│ ├── tmdbAPI.js
│ └── video_matroska.js
├── contentProviders
│ ├── 1fichier.js
│ ├── contentProvider.js
│ ├── dropbox.js
│ ├── file.js
│ ├── googleDrive.js
│ ├── http.js
│ └── hubic.js
├── db
│ ├── abstractRegistry.js
│ ├── cachedRegistry.js
│ ├── memoryRegistry.js
│ ├── mongodbRegistry.js
│ ├── mysqlRegistry.js
│ └── nedbRegistry.js
├── filterSearchEngine.js
├── i18n
│ ├── de.js
│ ├── en.js
│ ├── fr.js
│ ├── kr.js
│ └── lt.js
├── logger.js
├── mediaReceiverRegistrarService.js
├── node.js
├── repositories
│ ├── directory.js
│ ├── history.js
│ ├── iceCast.js
│ ├── movie.js
│ ├── music.js
│ ├── path.js
│ ├── repository.js
│ ├── scanner.js
│ ├── virtual.js
│ └── whatsNewRepository.js
├── service.js
├── stateVar.js
├── upnpServer.js
├── util
│ ├── alphaNormalizer.js
│ ├── asyncEventEmitter.js
│ ├── errorSoap.js
│ ├── jstoxml.js
│ ├── namedSemaphore.js
│ ├── nodeWeakHashmap.js
│ ├── semaphore.js
│ ├── url.js
│ ├── xmlFilters.js
│ └── xmldoc.js
└── xmlns.js
├── package.json
└── test
└── api.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /x1.xml
2 | /x2.xml
3 | /node_modules
4 | lib-cov
5 | *.seed
6 | *.log
7 | *.csv
8 | *.dat
9 | *.out
10 | *.pid
11 | *.gz
12 | .settings
13 |
14 | pids
15 | logs
16 | results
17 |
18 | npm-debug.log
19 | node_modules
20 |
21 | /server.js
22 | /.project
23 | /.tern-project
24 | /test-1fichier.json
25 | /.cache/
26 | /upnpserver.iml
27 | /.idea/
28 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 6,
3 | "boss": true,
4 | "node": true,
5 | "strict": false,
6 | "maxlen": 120,
7 | "newcap": false,
8 | "undef": true,
9 | "unused": true,
10 | "lastsemic": true,
11 | "laxcomma": true,
12 | "unused": false,
13 | "indent": 2,
14 | "globals": {
15 | "describe": false,
16 | "it": false,
17 | "module": false,
18 | "require": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | @./node_modules/.bin/mocha --recursive
3 |
4 | .PHONY: test
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Build Status][travis-image]][travis-url] [![NPM version][npm-image]][npm-url]
2 |
3 | # upnpserver
4 | 
5 |
6 |
7 | UpnpServer is a fast and light UPnP server written in NodeJS.
8 | This version does not need an external database (mysql, mongodb), it stores all information in memory.
9 |
10 |
11 | ## Compatibility
12 |
13 | - Freebox HD
14 | - Soundbridge
15 | - ht5streamer
16 | - Intel Device Validator
17 | - Samsung AllShare play
18 | - LG Smart Share
19 | - Android
20 | - VPlayer (with UPNP Plugin)
21 | - NX Player
22 |
23 | ## Installation
24 |
25 | $ npm install upnpserver
26 |
27 | ## Command line
28 |
29 | For command line, install [upnpserver-cli](https://github.com/oeuillot/upnpserver-cli) package.
30 |
31 | ## API Usage
32 |
33 | ```javascript
34 | var Server = require("upnpserver");
35 |
36 | var server = new Server({ /* configuration, see below */ }, [
37 | '/home/disk1',
38 | { path: '/home/myDisk' },
39 | { path: '/media/movies', mountPoint: '/My movies'},
40 | { path: '/media/albums', mountPoint: '/Personnal/My albums', type: 'music' }
41 | ]);
42 |
43 | server.start();
44 | ```
45 |
46 | ##Configuration
47 | Server constructor accepts an optional configuration object. At the moment, the following is supported:
48 |
49 | - `log` _Boolean_ Enable/disable logging. Default: false.
50 | - `logLevel` _String_ Specifies log level to print. Possible values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`. Defaults to `ERROR`.
51 | - `name` _String_ Name of server. Default 'Node Server'
52 | - `modelName` _String_ Model name of server. Default 'Node upnpserver'
53 | - `uuid` _String_ UUID of server. (If not specified, a UUID v4 will be generated)
54 | - `hostname` _String_ Hostname to bind the server. Default: 0.0.0.0
55 | - `httpPort` _Number_ Http port. Default: 10293
56 | - `dlnaSupport` _Boolean_ Enable/disable dlna support. Default: true
57 | - `strict` _Boolean_ Use only official UPnP attributes. Default: false
58 | - `lang` _String_ Specify the language (en, fr) for virtual folder names. Default: en
59 | - `ssdpLog` _Boolean_ Enable log of ssdp layer. Default: false
60 | - `ssdpLogLevel` _String_ Log level of ssdp layer.
61 |
62 | ## Testing
63 | For testing purposes used *mocha* framework. To run tests, you should do this:
64 | ```bash
65 | make test
66 | ```
67 |
68 | ## Author
69 |
70 | Olivier Oeuillot
71 |
72 | ## Contributors
73 |
74 | https://github.com/oeuillot/upnpserver/graphs/contributors
75 |
76 | [npm-url]: https://npmjs.org/package/upnpserver
77 | [npm-image]: https://badge.fury.io/js/upnpserver.svg
78 | [npm-downloads-image]: http://img.shields.io/npm/dm/upnpserver.svg
79 |
80 | [travis-url]: https://travis-ci.org/oeuillot/upnpserver
81 | [travis-image]: https://api.travis-ci.org/oeuillot/upnpserver.svg?branch=master
82 |
--------------------------------------------------------------------------------
/default-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "upnpClasses": {
3 | "object.item": "./lib/class/object.item",
4 | "object.container": "./lib/class/object.container",
5 |
6 | "object.item.audioItem": "./lib/class/object.item.audioItem",
7 | "object.item.audioItem.musicTrack": "./lib/class/object.item.audioItem.musicTrack",
8 | "object.item.audioItem.audioBroadcast": "./lib/class/object.item.audioItem.audioBroadcast",
9 |
10 | "object.item.videoItem": "./lib/class/object.item.videoItem",
11 | "object.item.videoItem.movie": "./lib/class/object.item.videoItem.movie",
12 | "object.item.videoItem.musicVideoClip": "./lib/class/object.item.videoItem.musicVideoClip",
13 | "object.item.videoItem.videoBroadcast": "./lib/class/object.item.videoItem.videoBroadcast",
14 |
15 | "object.item.imageItem": "./lib/class/object.item.imageItem",
16 | "object.item.imageItem.photo": "./lib/class/object.item.imageItem.photo",
17 |
18 | "object.item.textItem": "./lib/class/object.item.textItem",
19 |
20 | "object.container.album": "./lib/class/object.container.album",
21 | "object.container.album.musicAlbum": "./lib/class/object.container.album.musicAlbum",
22 | "object.container.album.photoAlbum": "./lib/class/object.container.album.photoAlbum",
23 | "object.container.album.photoAlbum.dateTaken": "./lib/class/object.container.album.photoAlbum.dateTaken",
24 | "object.container.album.videoAlbum": "./lib/class/object.container.album.videoAlbum",
25 |
26 | "object.container.person": "./lib/class/object.container.person",
27 | "object.container.person.musicArtist": "./lib/class/object.container.person.musicArtist",
28 | "object.container.person.movieActor": "./lib/class/object.container.person.movieActor",
29 |
30 | "object.container.genre": "./lib/class/object.container.genre",
31 | "object.container.genre.musicGenre": "./lib/class/object.container.genre.musicGenre",
32 | "object.container.genre.videoGenre": "./lib/class/object.container.genre.videoGenre",
33 |
34 | "object.container.playlistContainer": "./lib/class/object.container.playlistContainer"
35 | },
36 |
37 | "contentHandlers": [ {
38 | "key": "ffprobe",
39 | "mimeTypes": ["video/*", "audio/*" ],
40 | "type": "ffprobe",
41 | "priority": 25
42 | }, {
43 | "key": "matroska",
44 | "mimeType": "video/x-matroska",
45 | "type": "video_matroska",
46 | "priority": 20
47 | }, {
48 | "key": "musicmetadata",
49 | "mimeType": "audio/*",
50 | "type": "audio_musicmetadata",
51 | "priority": 20
52 | }, {
53 | "key": "exif",
54 | "mimeTypes": ["image/jpeg", "image/jp2" ],
55 | "type": "exif",
56 | "priority": 20
57 | }, {
58 | "key": "allo",
59 | "mimeType": "video/*",
60 | "type": "allo",
61 | "priority": -10
62 | }, {
63 | "key": "tmdb",
64 | "mimeTypes": ["inode/directory", "video/*" ],
65 | "type": "tmdb",
66 | "priority": -10
67 | }, {
68 | "key": "srt",
69 | "mimeType": "video/*",
70 | "type": "srt",
71 | "priority": -50
72 | } ],
73 |
74 | "enableIntelToolkitSupport": false,
75 |
76 | "contentProviders": [ {
77 | "name": "file",
78 | "protocol": "file",
79 | "type": "file"
80 | }
81 | ]
82 | }
83 |
--------------------------------------------------------------------------------
/examples/configuration-1fichier.json:
--------------------------------------------------------------------------------
1 | {
2 | "contentProviders": [ {
3 | "protocol": "1fichier",
4 | "username": "${1FICHIER_USERNAME}",
5 | "password": "${1FICHIER_PASSWORD}"
6 | }
7 | ],
8 | "repositories": [ {
9 | "type": "music",
10 | "path": "1fichier:/",
11 | "mountPath": "/music"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/examples/configuration-icecast.json:
--------------------------------------------------------------------------------
1 | {
2 | "repositories": [ {
3 | "type": "icecast",
4 | "mountPath": "/"
5 | }
6 | ]
7 | }
--------------------------------------------------------------------------------
/examples/configuration-repositories.json:
--------------------------------------------------------------------------------
1 | {
2 | "repositories": [ {
3 | "type": "movie",
4 | "mountPath": "/video",
5 | "path": "c:\\users\\oeuillot\\Downloads"
6 | }, {
7 | "type": "music",
8 | "mountPath": "/music",
9 | "path": "c:\\users\\oeuillot\\Music\\iTunes\\iTunes Media"
10 | }, {
11 | "type": "icecast",
12 | "mountPath": "/radio"
13 | },
14 | {
15 | "type": "directory",
16 | "mountPath": "/directory",
17 | "path": "C:\\temp\\scanner"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/examples/configuration-tmdb.json:
--------------------------------------------------------------------------------
1 | {
2 | "repositories": [
3 | {
4 | "type": "directory",
5 | "mountPath": "/directory",
6 | "path": "C:\\temp\\scanner"
7 | }
8 | ]
9 | }
--------------------------------------------------------------------------------
/icon/README.md:
--------------------------------------------------------------------------------
1 | # Icon
2 | 
3 |
4 | The Icon is the mix of UPnP and Node.JS logotypes. 'Play' icon is placed on Node.JS logotype's green hexagon.
5 |
6 | # Colors
7 | The Icon has 3 colors: atlantis (light green), mantis (green) and white.
8 |
9 | Name | HEX | RGB | CMYK
10 | -------- | ------- | ------------- | ------------------
11 | Atlantis | #83cd29 | 131, 205, 41 | 36, 0, 80, 20
12 | Mantis | #66c659 | 102, 198, 89 | 48, 0, 55, 22
13 | White | #ffffff | 255, 255, 255 | 0, 0, 0, 0
14 |
--------------------------------------------------------------------------------
/icon/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/icon/icon_128.png
--------------------------------------------------------------------------------
/icon/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/icon/icon_32.png
--------------------------------------------------------------------------------
/icon/icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/icon/icon_512.png
--------------------------------------------------------------------------------
/icon/icon_scalable.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/class/UPNP_attributes_for_class_items.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const DIDL_ATTRS = [ 'id', 'res', 'searchable', 'parentID', 'refID',
5 | 'restricted', 'childCount' ];
6 | const DC_ATTRS = [ 'title', 'date', 'creator', 'publisher', 'contributor',
7 | 'relation', 'description', 'rights', 'date', 'language' ];
8 | const UPNP_ATTRS = [ 'class', 'searchClass', 'createClass', 'writeStatus',
9 | 'artist', 'actor', 'author', 'producer', 'director', 'genre', 'album',
10 | 'playlist', 'longDescription', 'icon', 'region', 'rating', 'radioCallSign',
11 | 'radioStationID', 'radioBand', 'channelNr', 'channelName',
12 | 'originalTrackNumber', 'toc' ];
13 |
14 | const MUSICTRACK_DC = [ 'date', 'contributor' ];
15 | const MUSICTRACK_UPNP = [ 'artist', 'album', 'originalTrackNumber', 'playlist',
16 | 'storageMedium' ];
17 |
18 | const AUDIOITEM_DC = [ 'description', 'publisher', 'language', 'relation',
19 | 'rights' ];
20 | const AUDIOITEM_UPNP = [ 'genre', 'longDescription' ];
21 |
22 | const AUDIOBROADCAST_DC = [];
23 | const AUDIOBROADCAST_UPNP = [ 'region', 'radioCallSign', 'radioStationID',
24 | 'radioBand', 'channelNr' ];
25 |
26 | const AUDIOBOOK_DC = [ 'date', 'contributor' ];
27 | const AUDIOBOOK_UPNP = [ 'producer', 'storageMedium' ];
28 |
29 | const VIDEOITEM_DC = [ 'publisher', 'relation', 'description', 'language' ];
30 | const VIDEOITEM_UPNP = [ 'actor', 'producer', 'director', 'genre',
31 | 'longDescription', 'rating' ];
32 |
33 | const MOVIE_DC = [];
34 | const MOVIE_UPNP = [ 'storageMedium', 'DVDRegionCode', 'channelName',
35 | 'scheduledStartTime', 'scheduledEndTime' ];
36 |
37 | const VIDEOBRAODCAST_UPNP = [ 'icon', 'region', 'channelNr' ];
38 |
39 | const MUSICVIDEOCLIP_UPNP = [ 'artist', 'storageMedium', 'album',
40 | 'scheduledStartTime', 'scheduledEndTime', 'director' ];
41 | const MUSICVIDEOCLIP_DC = [ 'contributor', 'date' ];
42 |
43 | const IMAGEITEM_UPNP = [ 'longDescription', 'storageMedium', 'rating' ];
44 | const IMAGEITEM_DC = [ 'description', 'publisher', 'date', 'rights' ];
45 |
46 | const PHOTO_UPNP = [ 'album' ];
47 |
48 | const PLAYLIST_UPNP = [ 'artist', 'genre', 'longDescription', 'storageMedium' ];
49 | const PLAYLIST_DC = [ 'description', 'date', 'language' ];
50 |
51 | const TEXTITEM_UPNP = [ 'author', 'protection', 'longDescription',
52 | 'storageMedium', 'rating' ];
53 | const TEXTITEM_DC = [ 'description', 'publisher', 'contributor', 'date',
54 | 'relation', 'language', 'rights' ];
55 |
56 | const ALBUM_UPNP = [ 'storageMedium' ];
57 | const ALBUM_DC = [ 'longDescription', 'description', 'publisher', 'contributor',
58 | 'date', 'relation', 'rights' ];
59 |
60 | const MUSICALBUM_UPNP = [ 'artist', 'genre', 'producer', 'albumArtURI', 'toc' ];
61 |
62 | const GENRE_UPNP = [ 'longDescription' ];
63 | const GENRE_DC = [ 'description' ];
64 |
--------------------------------------------------------------------------------
/lib/class/object.container.album.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Container = require('./object.container');
5 |
6 | const _UPNP_CLASS=Container.UPNP_CLASS + ".album";
7 |
8 | class Album extends Container {
9 | get name() { return Album.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = Album;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.album.musicAlbum.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Item = require('./object.item');
5 | const Album = require('./object.container.album');
6 | const ImageItem = require('./object.item.imageItem');
7 | const Xmlns = require('../xmlns');
8 |
9 | const logger = require('../logger');
10 |
11 | const MAX_ALBUM_ARTS = 8;
12 |
13 | const MERGE_CHILD_ARTISTS = false;
14 |
15 | const MAX_ARTISTS = 4;
16 |
17 | const _UPNP_CLASS=Album.UPNP_CLASS + ".musicAlbum";
18 |
19 | class MusicAlbum extends Album {
20 | get name() { return MusicAlbum.UPNP_CLASS; }
21 |
22 | get defaultSort() { return [ "+upnp:originalTrackNumber", "+dc:title" ];}
23 |
24 | static get UPNP_CLASS() {
25 | return _UPNP_CLASS;
26 | }
27 |
28 | toJXML(node, attributes, request,
29 | filterCallback, callback) {
30 |
31 | super.toJXML(node, attributes, request, filterCallback,
32 | (error, xml) => {
33 | if (error) {
34 | return callback(error);
35 | }
36 |
37 | if (!filterCallback(Xmlns.UPNP_METADATA, "artist") &&
38 | !filterCallback(Xmlns.UPNP_METADATA, "albumArtURI")) {
39 | return callback(null, xml);
40 | }
41 |
42 | var getArtists=filterCallback(Xmlns.UPNP_METADATA, "artist");
43 |
44 |
45 | node.listChildren({
46 | resolveLinks : true
47 |
48 | }, (error, list) => {
49 | if (error) {
50 | logger.error("Can not list children");
51 | return callback(null, xml);
52 | }
53 |
54 | var count = 0;
55 | var artHash={};
56 | var artists = {};
57 | var artistsList = [];
58 |
59 | list.forEach((child) => {
60 | var childAttributes = child.attributes;
61 |
62 | if (!childAttributes) {
63 | return;
64 | }
65 |
66 | if (getArtists) {
67 | if (MERGE_CHILD_ARTISTS && childAttributes.artists) {
68 | childAttributes.artists.find((artist) => {
69 | if (artistsList.length>MAX_ARTISTS) {
70 | return true;
71 | }
72 |
73 | if (!artist || artists[artist.name || artist]) {
74 | return false;
75 | }
76 |
77 | artists[artist.name || artist] = true;
78 | artistsList.push(artist);
79 | });
80 | }
81 | }
82 |
83 | if (filterCallback(Xmlns.UPNP_METADATA, "albumArtURI")) {
84 | if (!MAX_ALBUM_ARTS || count < MAX_ALBUM_ARTS) {
85 | var albumArts = childAttributes.albumArts;
86 | if (albumArts) {
87 | albumArts.find((albumArtInfo) => {
88 | if (MAX_ALBUM_ARTS > 0 && count > MAX_ALBUM_ARTS) {
89 | return true;
90 | }
91 |
92 | if (ImageItem.isMimeTypeImage(albumArtInfo.mimeType)) {
93 | if (albumArtInfo.hash) {
94 | if (artHash[albumArtInfo.hash]) {
95 | return false;
96 | }
97 | artHash[albumArtInfo.hash]=true;
98 | }
99 |
100 | var aau = {
101 | _name : "upnp:albumArtURI",
102 | _content : request.contentURL + child.id +
103 | "/" + albumArtInfo.contentHandlerKey + "/" + albumArtInfo.key
104 | };
105 |
106 | if (request.dlnaSupport) {
107 | var dlna = albumArtInfo.dlnaProfile ||
108 | ImageItem.getDLNA(albumArtInfo.mimeType);
109 | if (dlna) {
110 | aau._attrs = {
111 | "dlna:profileID" : dlna
112 | };
113 | }
114 | }
115 |
116 | xml._content.push(aau);
117 |
118 | count++;
119 | }
120 | });
121 | }
122 | }
123 | }
124 | });
125 |
126 | if (getArtists) {
127 | Item.addList(xml._content, artistsList.length, "upnp:artist", true);
128 | }
129 |
130 | callback(null, xml);
131 | });
132 | });
133 | }
134 | }
135 |
136 | module.exports=MusicAlbum;
137 |
--------------------------------------------------------------------------------
/lib/class/object.container.album.photoAlbum.dateTaken.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const PhotoAlbum = require('./object.container.album.photoAlbum');
5 |
6 | const _UPNP_CLASS = PhotoAlbum.UPNP_CLASS + ".dateTaken";
7 |
8 | class DateTaken extends PhotoAlbum {
9 | get name() { return DateTaken.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = DateTaken;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.album.photoAlbum.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Album = require('./object.container.album');
5 |
6 | const _UPNP_CLASS =Album.UPNP_CLASS + ".photoAlbum";
7 |
8 | class PhotoAlbum extends Album {
9 | get name() { return PhotoAlbum.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = PhotoAlbum;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.album.videoAlbum.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Util = require('util');
5 |
6 | const Album = require('./object.container.album');
7 |
8 | const _UPNP_CLASS = Album.UPNP_CLASS + ".tvShows";
9 |
10 | class TvShows extends Album {
11 | get name() { return TvShows.UPNP_CLASS; }
12 |
13 | static get UPNP_CLASS() {
14 | return _UPNP_CLASS;
15 | }
16 | }
17 |
18 | module.exports = TvShows;
19 |
--------------------------------------------------------------------------------
/lib/class/object.container.audioContainer.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Container = require('./object.container');
5 |
6 | const _UPNP_CLASS = Container.UPNP_CLASS + ".audioContainer";
7 |
8 | class AudioContainer extends Container {
9 | get name() {
10 | return AudioContainer.UPNP_CLASS;
11 | }
12 |
13 | static get UPNP_CLASS() {
14 | return _UPNP_CLASS;
15 | }
16 | }
17 |
18 | module.exports = AudioContainer;
19 |
--------------------------------------------------------------------------------
/lib/class/object.container.genre.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Container = require('./object.container');
5 |
6 | const _UPNP_CLASS = Container.UPNP_CLASS + ".genre";
7 |
8 | class Genre extends Container {
9 | get name () {
10 | return Genre.UPNP_CLASS;
11 | }
12 |
13 | static get UPNP_CLASS() {
14 | return _UPNP_CLASS;
15 | }
16 | }
17 |
18 | module.exports=Genre;
19 |
--------------------------------------------------------------------------------
/lib/class/object.container.genre.musicGenre.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Genre = require('./object.container.genre');
5 |
6 | const _UPNP_CLASS = Genre.UPNP_CLASS + ".musicGenre";
7 |
8 | class MusicGenre extends Genre {
9 | get name() { return MusicGenre.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = MusicGenre;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.genre.videoGenre.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Genre = require('./object.container.genre');
5 |
6 | const _UPNP_CLASS = Genre.UPNP_CLASS + ".videoGenre";
7 |
8 | class VideoGenre extends Genre {
9 | get name() { return VideoGenre.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = VideoGenre;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Res = require('./object.res');
5 | const ObjectClass = require('./object');
6 |
7 | const FORCE_CHILDREN_COUNT=false;
8 |
9 | const _UPNP_CLASS = ObjectClass.UPNP_CLASS+".container";
10 |
11 | class Container extends Res {
12 | constructor() {
13 | super();
14 | }
15 |
16 | static get UPNP_CLASS() {
17 | return _UPNP_CLASS;
18 | }
19 |
20 | get mimeTypes() {
21 | if (Object.getPrototypeOf(this)!==Container.prototype) {
22 | return super.mimeTypes;
23 | }
24 | return [ 'inode/directory' ];
25 | }
26 |
27 | get name() {
28 | return Container.UPNP_CLASS;
29 | }
30 |
31 | get isContainer() { return true; }
32 |
33 | get defaultSort() { return [ "+dc:title" ]; }
34 |
35 | /**
36 | *
37 | */
38 | toJXML(node, attributes, request, filterCallback, callback) {
39 |
40 | super.toJXML(node, attributes, request, filterCallback, (error, xml) => {
41 | if (error) {
42 | return callback(error);
43 | }
44 |
45 | xml._name = "container";
46 | if (node.searchable) {
47 | xml._attrs.searchable = true;
48 | }
49 |
50 | var childrenIds = node.childrenIds;
51 | if (childrenIds!==undefined) {
52 | // Can not filter list ! future defect ?
53 | xml._attrs.childCount = childrenIds.length;
54 | return callback(null, xml);
55 | }
56 |
57 | if (!FORCE_CHILDREN_COUNT) {
58 | return callback(null, xml);
59 | }
60 |
61 | node.browseChildren( { request: request }, (error, list) => {
62 | if (error) {
63 | return callback(error);
64 | }
65 |
66 | node.service.emit("filterList", request, node, list);
67 |
68 | xml._attrs.childCount = (list) ? list.length : 0;
69 |
70 | callback(null, xml);
71 | });
72 | });
73 | }
74 | }
75 |
76 | module.exports = Container;
77 |
--------------------------------------------------------------------------------
/lib/class/object.container.person.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Container = require('./object.container');
5 |
6 | const _UPNP_CLASS = Container.UPNP_CLASS + ".person";
7 |
8 | class Person extends Container {
9 | get name() { return Person.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = Person;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.person.movieActor.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Person = require('./object.container.person');
5 |
6 | const _UPNP_CLASS = Person.UPNP_CLASS + ".movieActor";
7 |
8 | class MovieActor extends Person {
9 | get name() { return MovieActor.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = MovieActor;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.person.musicArtist.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Person = require('./object.container.person');
5 |
6 | const _UPNP_CLASS = Person.UPNP_CLASS + ".musicArtist";
7 |
8 | class MusicArtist extends Person {
9 | get name() { return MusicArtist.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = MusicArtist;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.playlistContainer.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Container = require('./object.container');
5 |
6 | const _UPNP_CLASS = Container.UPNP_CLASS + ".playlistContainer";
7 |
8 | class PlaylistContainer extends Container {
9 | get name() { return PlaylistContainer.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = PlaylistContainer;
17 |
--------------------------------------------------------------------------------
/lib/class/object.container.tvShows.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Util = require('util');
5 |
6 | const Album = require('./object.container.album');
7 |
8 | const _UPNP_CLASS = Album.UPNP_CLASS + ".videoAlbum";
9 |
10 | class VideoAlbum extends Album {
11 | get name() { return VideoAlbum.UPNP_CLASS; }
12 |
13 | static get UPNP_CLASS() {
14 | return _UPNP_CLASS;
15 | }
16 | }
17 |
18 | module.exports = VideoAlbum;
19 |
--------------------------------------------------------------------------------
/lib/class/object.item.audioItem.audioBook.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const AudioItem = require('./object.item.audioItem');
5 |
6 | const _UPNP_CLASS = AudioItem.UPNP_CLASS + ".audioBook";
7 |
8 | class AudioBook extends AudioItem {
9 | get name() { return AudioBook.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = AudioBook;
17 |
--------------------------------------------------------------------------------
/lib/class/object.item.audioItem.audioBroadcast.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const AudioItem = require('./object.item.audioItem');
5 |
6 | const _UPNP_CLASS = AudioItem.UPNP_CLASS + ".audioBroadcast";
7 |
8 | class AudioBroadcast extends AudioItem {
9 | get name() { return AudioBroadcast.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = AudioBroadcast;
17 |
--------------------------------------------------------------------------------
/lib/class/object.item.audioItem.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Res = require('./object.res');
5 | const Item = require('./object.item');
6 |
7 | const _UPNP_CLASS = Item.UPNP_CLASS + ".audioItem";
8 |
9 | class AudioItem extends Res {
10 | get name() {
11 | return AudioItem.UPNP_CLASS;
12 | }
13 |
14 | static get UPNP_CLASS() {
15 | return _UPNP_CLASS;
16 | }
17 |
18 | getDLNA_ProfileName(item) {
19 | switch (item.attributes.mimeType) {
20 | case "audio/mpeg":
21 | return "MP3";
22 |
23 | // Thanks to s-leger
24 | case "audio/ogg":
25 | return "OGG";
26 |
27 | case "audio/aac":
28 | return "AAC";
29 |
30 | case "audio/aacp":
31 | return "AAC";
32 |
33 | case "audio/L16":
34 | return "LPCM";
35 |
36 | case "audio/L16p":
37 | return "LPCM";
38 | }
39 |
40 | return super.getDLNA_ProfileName(item);
41 | }
42 | }
43 |
44 | module.exports = AudioItem;
45 |
--------------------------------------------------------------------------------
/lib/class/object.item.audioItem.musicTrack.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const AudioItem = require('./object.item.audioItem');
5 | const Item = require('./object.item');
6 | const Xmlns = require('../xmlns');
7 |
8 | const MUSICMEDATA_LIST = [ 'trackOf', 'diskNo', 'diskOf' ];
9 |
10 | const _UPNP_CLASS = AudioItem.UPNP_CLASS + ".musicTrack";
11 |
12 | class MusicTrack extends AudioItem {
13 | get name() { return MusicTrack.UPNP_CLASS; }
14 |
15 | get mimeTypes() { return [ 'audio/*' ]; }
16 |
17 | static get UPNP_CLASS() {
18 | return _UPNP_CLASS;
19 | }
20 |
21 | toJXML(node, attributes, request, filterCallback, callback) {
22 |
23 | super.toJXML(node, attributes, request, filterCallback, (error, xml) => {
24 | if (error) {
25 | return callback(error);
26 | }
27 |
28 | var content = xml._content;
29 |
30 | if (filterCallback(Xmlns.UPNP_METADATA, "album")) {
31 | if (attributes.album) {
32 | content.push({
33 | _name : "upnp:album",
34 | _content : attributes.album
35 | });
36 | }
37 | }
38 |
39 | if (filterCallback(Xmlns.UPNP_METADATA, "originalTrackNumber")) {
40 | if (typeof(attributes.originalTrackNumber)==="number") {
41 | content.push({
42 | _name : "upnp:originalTrackNumber",
43 | _content : attributes.originalTrackNumber
44 | });
45 | }
46 | }
47 |
48 | if (filterCallback(Xmlns.UPNP_METADATA, "originalDiscNumber")) {
49 | if (typeof(attributes.originalDiscNumber)==="number") {
50 | content.push({
51 | _name : "upnp:originalDiscNumber",
52 | _content : attributes.originalDiscNumber
53 | });
54 | }
55 | }
56 |
57 | if (request.jasminMusicMetadatasSupport) {
58 | MUSICMEDATA_LIST.forEach((name) => {
59 | var value = attributes[name];
60 | if (value === undefined) {
61 | return;
62 | }
63 |
64 | if (!filterCallback(Xmlns.JASMIN_MUSICMETADATA, name)) {
65 | return;
66 | }
67 |
68 | var x = {
69 | _name : "mm:" + name,
70 | _content : value
71 | };
72 |
73 | content.push(x);
74 | });
75 | }
76 |
77 | callback(null, xml);
78 | });
79 | }
80 | }
81 |
82 | module.exports = MusicTrack;
83 |
--------------------------------------------------------------------------------
/lib/class/object.item.imageItem.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Res = require('./object.res');
5 | const Item = require('./object.item');
6 |
7 | const _UPNP_CLASS = Item.UPNP_CLASS + ".imageItem";
8 |
9 | class ImageItem extends Res {
10 | get name() { return ImageItem.UPNP_CLASS; }
11 |
12 | get mimeTypes() { return [ 'image/*' ]; }
13 |
14 | static get UPNP_CLASS() {
15 | return _UPNP_CLASS;
16 | }
17 |
18 | getDLNA_ProfileName(item) {
19 |
20 | var attributes = item.attributes;
21 | var w = attributes.width;
22 | var h = attributes.height;
23 |
24 | var dlna=ImageItem.getDLNA(attributes.mimeType, w, h);
25 | if (dlna) {
26 | return dlna;
27 | }
28 |
29 | return super.getDLNA_ProfileName(item);
30 | }
31 |
32 | static isMimeTypeImage(mimeType) {
33 | return mimeType && mimeType.indexOf("image/") === 0;
34 | }
35 |
36 | static getDLNA(mimeType, w, h) {
37 |
38 | switch (mimeType) {
39 | case "image/jpeg":
40 | if (w > 0 && h > 0) {
41 | if (w === 48 && h === 48) {
42 | return "JPEG_SM_ICO";
43 | }
44 |
45 | if (w === 120 && h === 120) {
46 | return "JPEG_LRG_ICO";
47 | }
48 |
49 | if (w <= 160 && h <= 160) {
50 | return "JPEG_TN";
51 | }
52 |
53 | if (w <= 640 && h <= 480) {
54 | return "JPEG_SM";
55 | }
56 |
57 | if (w <= 1024 && h <= 768) {
58 | return "JPEG_MED";
59 | }
60 |
61 | if (w <= 4096 && h <= 4096) {
62 | return "JPEG_LRG";
63 | }
64 | return "";
65 | }
66 |
67 | return "JPEG_LRG";
68 |
69 | case "image/png":
70 | if (w > 0 && h > 0) {
71 | if (w === 48 && h === 48) {
72 | return "PNG_SM_ICO";
73 | }
74 |
75 | if (w === 120 && h === 120) {
76 | return "PNG_LRG_ICO";
77 | }
78 |
79 | if (w <= 160 && h <= 160) {
80 | return "PNG_TN";
81 | }
82 |
83 | /*
84 | if (w <= 640 && h <= 480) {
85 | return "PNG_SM";
86 | }
87 | */
88 |
89 | if (w <= 4096 && h <= 4096) {
90 | return "PNG_LRG";
91 | }
92 | return "";
93 | }
94 |
95 | return "PNG_LRG";
96 | }
97 |
98 | return undefined;
99 | }
100 | }
101 |
102 | module.exports = ImageItem;
103 |
--------------------------------------------------------------------------------
/lib/class/object.item.imageItem.photo.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const ImageItem = require('./object.item.imageItem');
5 |
6 | const _UPNP_CLASS = ImageItem.UPNP_CLASS + ".photo";
7 |
8 | class Photo extends ImageItem {
9 | get name() { return Photo.UPNP_CLASS; }
10 |
11 | get mimeTypes() { return [ 'image/jpeg', 'image/jp2' ]; }
12 |
13 | static get UPNP_CLASS() {
14 | return _UPNP_CLASS;
15 | }
16 | }
17 |
18 | module.exports = Photo;
19 |
--------------------------------------------------------------------------------
/lib/class/object.item.textItem.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Res = require('./object.res');
5 | const Item = require('./object.item');
6 |
7 | const _UPNP_CLASS = Item.UPNP_CLASS + ".textItem";
8 |
9 | class TextItem extends Res {
10 | get name() { return TextItem.UPNP_CLASS; }
11 |
12 | get mimeTypes() { return [ 'text/*' ]; }
13 |
14 | static get UPNP_CLASS() {
15 | return _UPNP_CLASS;
16 | }
17 | }
18 |
19 | module.exports=TextItem;
20 |
--------------------------------------------------------------------------------
/lib/class/object.item.videoItem.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const debug = require('debug')('upnpserver:class:object.item.videoItem');
6 |
7 | const Res = require('./object.res');
8 | const Item = require('./object.item');
9 | const Xmlns = require('../xmlns');
10 |
11 | const _UPNP_CLASS = Item.UPNP_CLASS + ".videoItem";
12 |
13 | class VideoItem extends Res {
14 | get name() { return VideoItem.UPNP_CLASS; }
15 |
16 | get mimeTypes() { return [ 'video/*' ]; }
17 |
18 | static get UPNP_CLASS() {
19 | return _UPNP_CLASS;
20 | }
21 |
22 | /**
23 | *
24 | */
25 | getDLNA_ProfileName(node) {
26 | switch (node.attributes.mimeType) {
27 | case "video/mpeg":
28 | return "MPEG_PS_PAL";
29 | }
30 |
31 | return super.getDLNA_ProfileName(node);
32 | }
33 |
34 | /**
35 | *
36 | */
37 | toJXML(node, attributes, request,
38 | filterCallback, callback) {
39 |
40 | super.toJXML(node, attributes, request, filterCallback,
41 | (error, xml) => {
42 | if (error) {
43 | return callback(error);
44 | }
45 |
46 | var content = xml._content;
47 |
48 | // http://192.168.0.191:17679/SubtitleProvider/41.SRT
49 | // xmlns:sec="http://www.sec.co.kr/dlna
50 |
51 | var description;
52 |
53 | if (filterCallback(Xmlns.PURL_ELEMENT, "description")) {
54 | if (attributes.description) {
55 | description = attributes.description;
56 |
57 | content.push({
58 | _name : "dc:description",
59 | _content : description
60 | });
61 | }
62 | }
63 |
64 | if (filterCallback(Xmlns.UPNP_METADATA, "longDescription")) {
65 | if (attributes.longDescription &&
66 | description !== attributes.longDescription) {
67 | content.push({
68 | _name : "upnp:longDescription",
69 | _content : attributes.longDescription
70 | });
71 | }
72 | }
73 |
74 | return callback(null, xml);
75 | });
76 | }
77 | }
78 |
79 | module.exports = VideoItem;
80 |
--------------------------------------------------------------------------------
/lib/class/object.item.videoItem.movie.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const VideoItem = require('./object.item.videoItem');
5 | const Xmlns = require('../xmlns');
6 |
7 | const _UPNP_CLASS = VideoItem.UPNP_CLASS + ".movie";
8 |
9 | class Movie extends VideoItem {
10 | get name() { return Movie.UPNP_CLASS; }
11 |
12 | get mimeTypes() { return [ 'video/mp4', 'video/x-matroska',
13 | 'video/x-msvideo' ]; }
14 |
15 | static get UPNP_CLASS() {
16 | return _UPNP_CLASS;
17 | }
18 |
19 | toJXML(node, attributes, request, filterCallback,
20 | callback) {
21 |
22 | super.toJXML(node, attributes, request,
23 | filterCallback, (error, xml) => {
24 | if (error) {
25 | return callback(error);
26 | }
27 |
28 | var content = xml._content;
29 |
30 | if (filterCallback(Xmlns.UPNP_METADATA, "region")) {
31 | if (attributes.region) {
32 | content.push({
33 | _name : "upnp:region",
34 | _content : attributes.region
35 | });
36 | }
37 | }
38 |
39 | if (request.jasminMovieMetadatasSupport) {
40 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "originalTitle")) {
41 | if (attributes.originalTitle) {
42 | content.push({
43 | _name : "mo:originalTitle",
44 | _content : attributes.originalTitle
45 | });
46 | }
47 | }
48 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "alsoKnownAs")) {
49 | if (attributes.titleAlsoKnownAs) {
50 | content.push({
51 | _name : "mo:alsoKnownAs",
52 | _content : attributes.titleAlsoKnownAs
53 | });
54 | }
55 | }
56 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "releaseDate")) {
57 | if (attributes.releaseDate) {
58 | content.push({
59 | _name : "mo:releaseDate",
60 | _content : attributes.releaseDate
61 | });
62 | }
63 | }
64 |
65 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "certificate")) {
66 | if (attributes.certificate) {
67 | content.push({
68 | _name : "mo:certificate",
69 | _content : attributes.certificate
70 | });
71 | }
72 | }
73 |
74 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "season")) {
75 | if (attributes.season!==undefined) {
76 | content.push({
77 | _name : "mo:season",
78 | _content : attributes.season
79 | });
80 | }
81 | }
82 |
83 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "episode")) {
84 | if (attributes.episode!==undefined) {
85 | content.push({
86 | _name : "mo:episode",
87 | _content : attributes.episode
88 | });
89 | }
90 | }
91 |
92 | if (filterCallback(Xmlns.JASMIN_MOVIEMETADATA, "airDate")) {
93 | if (attributes.airDate!==undefined) {
94 | content.push({
95 | _name : "mo:airDate",
96 | _content : attributes.airDate
97 | });
98 | }
99 | }
100 | }
101 |
102 | return callback(null, xml);
103 | });
104 | }
105 | }
106 |
107 | module.exports = Movie;
108 |
--------------------------------------------------------------------------------
/lib/class/object.item.videoItem.musicVideoClip.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const VideoItem = require('./object.item.videoItem');
5 |
6 | const _UPNP_CLASS = VideoItem.UPNP_CLASS + ".musicVideoClip";
7 |
8 | class MusicVideoClip extends VideoItem {
9 | get name() { return MusicVideoClip.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = MusicVideoClip;
17 |
--------------------------------------------------------------------------------
/lib/class/object.item.videoItem.videoBroadcast.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const VideoItem = require('./object.item.videoItem');
5 |
6 | const _UPNP_CLASS = VideoItem.UPNP_CLASS + ".videoBroadcast";
7 |
8 | class VideoBroadcast extends VideoItem {
9 | get name() { return VideoBroadcast.UPNP_CLASS; }
10 |
11 | static get UPNP_CLASS() {
12 | return _UPNP_CLASS;
13 | }
14 | }
15 |
16 | module.exports = VideoBroadcast;
17 |
--------------------------------------------------------------------------------
/lib/class/object.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, sub: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const debug = require('debug')('upnpserver:class:object.item');
6 | const Xmlns = require('../xmlns');
7 |
8 | const _UPNP_CLASS = "object";
9 |
10 | class ObjectClass {
11 |
12 | get name() {
13 | return ObjectClass.UPNP_CLASS;
14 | }
15 |
16 | get isContainer() {
17 | return false;
18 | }
19 |
20 | static get UPNP_CLASS() {
21 | return _UPNP_CLASS;
22 | }
23 |
24 |
25 | /**
26 | *
27 | */
28 | toString() {
29 | return "[UpnpClass " + this.name + "]";
30 | }
31 |
32 | /**
33 | * @param {subclass|string}
34 | * @return {boolean}
35 | */
36 | isSubClassOf(subclass) {
37 | if (subclass instanceof ObjectClass) {
38 | subclass=subclass.name;
39 | }
40 |
41 | return (subclass.indexOf(this.name)===0);
42 | }
43 | }
44 |
45 | module.exports = ObjectClass;
46 |
--------------------------------------------------------------------------------
/lib/class/object.res.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const Mime = require('mime');
6 | const debug = require('debug')('upnpserver:classes:object_res');
7 |
8 | const Item = require('./object.item');
9 | const Xmlns = require('../xmlns');
10 |
11 | var ImageItem;
12 |
13 | class Res extends Item {
14 | constructor() {
15 | super();
16 | if (!ImageItem) {
17 | ImageItem = require('./object.item.imageItem');
18 | }
19 | }
20 |
21 | /**
22 | *
23 | */
24 | toJXML(node, attributes, request, filterCallback, callback) {
25 |
26 | debug("toJXML", "node #",node.id, "attributes=",attributes);
27 |
28 | super.toJXML(node, attributes, request, filterCallback,
29 | (error, xml) => {
30 | if (error) {
31 | return callback(error);
32 | }
33 |
34 | var content = xml._content;
35 |
36 | if (filterCallback(Xmlns.UPNP_METADATA, "albumArtURI")) {
37 | if (attributes.albumArts) {
38 | var hashs = {};
39 |
40 | attributes.albumArts.forEach((albumArtInfo) => {
41 |
42 | if (ImageItem.isMimeTypeImage(albumArtInfo.mimeType)) {
43 | if (albumArtInfo.hash) {
44 | if (hashs[albumArtInfo.hash]) {
45 | return;
46 | }
47 | hashs[albumArtInfo.hash] = true;
48 | }
49 |
50 | var aau = {
51 | _name : "upnp:albumArtURI",
52 | _content : request.contentURL + node.id + "/" +
53 | albumArtInfo.contentHandlerKey + "/" + albumArtInfo.key
54 | };
55 |
56 | if (request.dlnaSupport) {
57 | var dlna = albumArtInfo.dlnaProfile || ImageItem.getDLNA(albumArtInfo.mimeType);
58 | if (dlna) {
59 | aau._attrs = {
60 | "dlna:profileID" : dlna
61 | };
62 | }
63 | }
64 |
65 | content.push(aau);
66 | }
67 | });
68 | }
69 | }
70 |
71 | if (filterCallback(Xmlns.DIDL_LITE, "res")) {
72 | var res = attributes.res;
73 | if (!res && (node.contentURL || attributes.externalContentURL)) {
74 | _addRes(xml, [ {} ], request, node, filterCallback);
75 | }
76 |
77 | if (res) {
78 | res.forEach((r) => {
79 | _addRes(xml, r, request, node, filterCallback);
80 | });
81 | }
82 | }
83 |
84 | if (true) {
85 | if (filterCallback(Xmlns.PURL_ELEMENT, "date") && node.contentTime) {
86 | var dcDate = Item.getXmlNode(xml, "dc:date");
87 | if (!dcDate._content && node.contentTime) {
88 | dcDate._content = Item.toISODate(node.contentTime);
89 | }
90 | }
91 | }
92 |
93 | callback(null, xml);
94 | });
95 | }
96 |
97 | /**
98 | *
99 | */
100 | getDLNA_ProfileName(item) {
101 | return "";
102 | }
103 | }
104 |
105 | function formatDuration(t) {
106 | var millis = Math.floor(t * 1000) % 1000;
107 | t = Math.floor(t);
108 |
109 | var seconds = t % 60;
110 | t = Math.floor(t / 60);
111 |
112 | var minutes = t % 60;
113 | t = Math.floor(t / 60);
114 |
115 | var hours = t;
116 |
117 | function pad(v, n) {
118 | var s = "0000" + v;
119 | return s.slice(-n);
120 | }
121 |
122 | return hours + ":" + pad(minutes, 2) + ":" + pad(seconds, 2); // + ":" +pad(millis, 3);
123 | }
124 |
125 | function format(attributeName, value) {
126 | if (attributeName === "duration") {
127 | if (typeof (value) === "number") {
128 | return formatDuration(value);
129 | }
130 | }
131 |
132 | return value;
133 | }
134 |
135 |
136 | const RES_PROPERTIES = [ 'size', 'duration', 'bitrate', 'sampleFrequency',
137 | 'bitsPerSample', 'nrAudioChannels', 'resolution', 'colorDepth', 'tspec',
138 | 'allowedUse', 'validityStart', 'validityEnd', 'remainingTime', 'usageInfo',
139 | 'rightsInfoURI', 'contentInfoURI', 'recordQuality', 'daylightSaving',
140 | 'framerate', 'importURI' ];
141 |
142 | function _addRes(xml, res, request, node, filterCallback) {
143 | var attributes = node.attributes;
144 |
145 | var key = res.key || "main";
146 | key=String(key).replace(/[\/ ]/g, '_'); // Key can be not a string
147 |
148 | var resAttributes = {
149 | id : key
150 | };
151 | if (!res.key && attributes.size) {
152 | resAttributes.size = attributes.size;
153 | }
154 |
155 | var contentFormat = res.mimeType;
156 | if (!contentFormat && !res.key) {
157 | contentFormat = attributes.mimeType || node.contentURL.mimeLookup() || '';
158 | }
159 |
160 | if (contentFormat==='inode/directory') {
161 | return;
162 | }
163 |
164 | if (filterCallback(Xmlns.DIDL_LITE, "res", "protocolInfo")) {
165 | var protocol = "http-get";
166 | var network = res.network || "*";
167 |
168 | var additionalInfo = res.additionalInfo;
169 |
170 | if (request.dlnaSupport) {
171 | var pn = res.dlnaProfile;
172 | if (!pn) {
173 | if (/^image\//.exec(contentFormat)) {
174 | pn = ImageItem.getDLNA(contentFormat, res.width, res.height);
175 | }
176 | }
177 |
178 | var adds = [];
179 | if (additionalInfo) {
180 | adds.push(additionalInfo);
181 | }
182 |
183 | if (pn) {
184 | adds.push("DLNA.ORG_PN=" + pn);
185 | }
186 | adds.push("DLNA.ORG_FLAGS=00f00000000000000000000000000000");
187 |
188 | additionalInfo = adds.join(";");
189 | }
190 |
191 | var attrs = [ protocol, network, contentFormat, additionalInfo || "*" ].join(":");
192 |
193 | resAttributes.protocolInfo = attrs;
194 | }
195 |
196 | RES_PROPERTIES.forEach(function(n) {
197 | if (!filterCallback(Xmlns.DIDL_LITE, "res", n)) {
198 | return;
199 | }
200 |
201 | var val = res[n];
202 | if (!val) {
203 | return;
204 | }
205 |
206 | resAttributes[n] = format(n, val);
207 | });
208 |
209 | if (!resAttributes.resolution) {
210 | if (res.width && res.height) {
211 | resAttributes.resolution = res.width + "x" + res.height;
212 | }
213 | }
214 |
215 | if (request.secDlnaSupport) {
216 | if (res.acodec) {
217 | if (filterCallback(Xmlns.DIDL_LITE, "res", "sec:acodec")) {
218 | resAttributes["sec:acodec"] = res.acodec;
219 | }
220 | }
221 |
222 | if (res.vcodec) {
223 | if (filterCallback(Xmlns.DIDL_LITE, "res", "sec:vcodec")) {
224 | resAttributes["sec:vcodec"] = res.vcodec;
225 | }
226 | }
227 | }
228 |
229 | var contentURL;
230 |
231 | if (!res.key) {
232 | contentURL = attributes.externalContentURL || (request.contentURL + node.id);
233 |
234 | } else {
235 | contentURL = request.contentURL + node.id + "/" + res.contentHandlerKey + "/" + res.key;
236 |
237 | if (res.paramURL) {
238 | contentURL+="/"+res.paramURL;
239 | }
240 | }
241 |
242 | xml._content.push({
243 | _name : "res",
244 | _attrs : resAttributes,
245 | _content : contentURL
246 | });
247 |
248 | if (request.secDlnaSupport) {
249 | if (contentFormat==="text/srt") {
250 | if (filterCallback(Xmlns.SEC_DLNA, "CaptionInfoEx")) {
251 | xml._content.push({
252 | _name : "sec:CaptionInfoEx",
253 | _attrs : {
254 | 'sec:type' : 'srt'
255 | },
256 | _content : contentURL
257 | });
258 | }
259 | }
260 | }
261 | }
262 |
263 | module.exports=Res;
264 |
--------------------------------------------------------------------------------
/lib/connectionManagerService.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, sub:true, esversion: 6 */
2 | "use strict";
3 |
4 | var Util = require('util');
5 |
6 | var Service = require("./service");
7 |
8 | class ConnectionManagerService extends Service {
9 | constructor() {
10 | super({
11 | serviceType : "urn:schemas-upnp-org:service:ConnectionManager:1",
12 | serviceId : "urn:upnp-org:serviceId:ConnectionManager",
13 | route : "cms"
14 | });
15 |
16 | this.addAction("GetCurrentConnectionIDs", [], [ {
17 | name : "ConnectionIDs",
18 | type : "CurrentConnectionIDs"
19 | } ]);
20 |
21 | this.addAction("GetCurrentConnectionInfo", [ {
22 | name : "ConnectionID",
23 | type : "A_ARG_TYPE_ConnectionID"
24 | } ], [ {
25 | name : "RcsID",
26 | type : "A_ARG_TYPE_RcsID"
27 | }, {
28 | name : "AVTransportID",
29 | type : "A_ARG_TYPE_AVTransportID"
30 | }, {
31 | name : "ProtocolInfo",
32 | type : "A_ARG_TYPE_ProtocolInfo"
33 | }, {
34 | name : "PeerConnectionManager",
35 | type : "A_ARG_TYPE_ConnectionManager"
36 | }, {
37 | name : "PeerConnectionID",
38 | type : "A_ARG_TYPE_ConnectionID"
39 | }, {
40 | name : "Direction",
41 | type : "A_ARG_TYPE_Direction"
42 | }, {
43 | name : "Status",
44 | type : "A_ARG_TYPE_ConnectionStatus"
45 | } ]);
46 |
47 | this.addAction("GetProtocolInfo", [], [ {
48 | name : "Source",
49 | type : "SourceProtocolInfo"
50 | }, {
51 | name : "Sink",
52 | type : "SinkProtocolInfo"
53 | } ]);
54 |
55 | // addType (name, type, value, valueList, ns, evented,
56 | // moderation_rate, additionalProps, preEventCb, postEventCb)
57 | this.addType("A_ARG_TYPE_ProtocolInfo", "string");
58 |
59 | this.addType("A_ARG_TYPE_ConnectionStatus", "string", "Unknown",
60 | [ "OK", "ContentFormatMismatch", "InsufficientBandwidth", "UnreliableChannel", "Unknown" ]);
61 | this.addType("A_ARG_TYPE_AVTransportID", "i4");
62 | this.addType("A_ARG_TYPE_RcsID", "i4");
63 | this.addType("A_ARG_TYPE_ConnectionID", "i4");
64 | this.addType("A_ARG_TYPE_ConnectionManager", "string");
65 | this.addType("SourceProtocolInfo", "string", "", [], null, true);
66 | this.addType("SinkProtocolInfo", "string", "", [], null, true);
67 | this.addType("A_ARG_TYPE_Direction", "string", "Output", [ "Input", "Output" ]);
68 | this.addType("CurrentConnectionIDs", "string");
69 | }
70 |
71 | initialize(upnpServer, callback) {
72 | // Kept here for intel upnp toolkit, but not in upnp spec
73 | super.initialize(upnpServer, () => {
74 | if (upnpServer.configuration.enableIntelToolkitSupport) {
75 | this._intervalTimer = setInterval(() => {
76 | this._sendPropertyChangesEvent();
77 | }, 1500);
78 | }
79 | return callback(null, this);
80 | });
81 | }
82 |
83 | //Kept here for intel upnp toolkit, but not in upnp spec
84 | _sendPropertyChangesEvent() {
85 |
86 | var stateVars = this.stateVars;
87 |
88 | var xmlContent = [];
89 | stateVars["CurrentConnectionIDs"].pushEventJXML(xmlContent);
90 | stateVars["SinkProtocolInfo"].pushEventJXML(xmlContent);
91 | stateVars["SourceProtocolInfo"].pushEventJXML(xmlContent);
92 |
93 | this.makeEvent(xmlContent);
94 | }
95 | }
96 |
97 | module.exports = ConnectionManagerService;
98 |
99 |
--------------------------------------------------------------------------------
/lib/contentHandlers/abstract_metas.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debug = require('debug')('upnpserver:contentHandlers:AbstractMetas');
5 | const logger = require('../logger');
6 |
7 | const ContentHandler = require('./contentHandler');
8 |
9 | const REQUEST_REGEXP = /^([^_]+)_(.+)$/i;
10 |
11 | class Abstract_Metas extends ContentHandler {
12 |
13 | /**
14 | *
15 | */
16 | getTrailerURL(contentURL, key, callback) {
17 | callback(new Error("Must be implemented !"));
18 | }
19 |
20 | /**
21 | *
22 | */
23 | getPosterURL(contentURL, key, callback) {
24 | callback(new Error("Must be implemented !"));
25 | }
26 |
27 | /**
28 | *
29 | */
30 | getStillURL(contentURL, key, callback) {
31 | callback(new Error("Must be implemented !"));
32 | }
33 |
34 | /**
35 | *
36 | */
37 | getBackdropURL(contentURL, key, callback) {
38 | callback(new Error("Must be implemented !"));
39 | }
40 |
41 | /**
42 | *
43 | */
44 | _getResourceContentURL(node, type, key, parameters, res, callback) {
45 |
46 | switch (type) {
47 | case "poster":
48 | this.getPosterURL(node, key, callback);
49 | return;
50 |
51 | case "trailer":
52 | this.getTrailerURL(node, key, callback);
53 | return;
54 |
55 | case "still":
56 | this.getStillURL(node, key, callback);
57 | return;
58 |
59 | case "backdrop":
60 | this.getBackdropURL(node, key, callback);
61 | return;
62 | }
63 |
64 | callback();
65 | }
66 |
67 | /**
68 | *
69 | */
70 | refTrailer(metas, contentURL, movieKey, callback) {
71 |
72 | this.getTrailerURL(contentURL, movieKey, (error, trailerURL) => {
73 | if (error) {
74 | debug("refTrailer", "ContentURL=", contentURL, "error=", error, error.stack);
75 | return callback(error);
76 | }
77 |
78 | trailerURL.stat((error, stats) => {
79 | debug("refTrailer", "Stat trailer path=", trailerURL, "=>", stats, "error=", error);
80 | if (error) {
81 | if (error.code === 'ENOENT') {
82 | return callback();
83 | }
84 | error.trailerURL = trailerURL;
85 | return callback(error);
86 | }
87 |
88 | metas.res = metas.res || [{}];
89 | metas.res.push({
90 | contentHandlerKey: this.name,
91 | key: "trailer_" + movieKey,
92 | mimeType: stats.mimeType,
93 | size: stats.size,
94 | additionalInfo: "type=trailer",
95 | mtime: stats.mtime.getTime()
96 | });
97 |
98 | callback();
99 | });
100 | });
101 | }
102 |
103 | /**
104 | *
105 | */
106 | refPoster(metas, contentURL, movieKey, callback) {
107 |
108 | this.getPosterURL(contentURL, movieKey, (error, posterURL) => {
109 | if (error) {
110 | var ex = new Error("Can not get posterURL");
111 | ex.movieKey = movieKey;
112 | ex.contentURL = contentURL;
113 | ex.error = error;
114 | return callback(ex);
115 | }
116 |
117 | posterURL.stat((error, stats) => {
118 | debug("refPoster", "Stat poster url=", posterURL, "=>", stats, "error=", error);
119 | if (error) {
120 | if (error.code === 'ENOENT') {
121 | return callback();
122 | }
123 |
124 | //logger.error("Can not stat url=",posterURL, error);
125 | error.posterURL = posterURL;
126 | error.movieKey = movieKey;
127 | return callback(error);
128 | }
129 |
130 | metas.res = metas.res || [{}];
131 | metas.res.push({
132 | contentHandlerKey: this.name,
133 | key: "poster_" + movieKey,
134 | mimeType: stats.mimeType,
135 | size: stats.size,
136 | additionalInfo: "type=poster",
137 | mtime: stats.mtime.getTime()
138 | });
139 |
140 | callback();
141 | });
142 | });
143 | }
144 |
145 | /**
146 | *
147 | */
148 | processRequest(node, request, response, path, parameters, callback) {
149 |
150 | var type = parameters[0];
151 | var resKey;
152 | var ret = REQUEST_REGEXP.exec(type);
153 | if (ret) {
154 | resKey = ret[2];
155 | type = ret[1];
156 | }
157 |
158 | debug("processRequest", "Parse Key", parameters, "=> type=", type, "resKey=", resKey);
159 |
160 | var res;
161 | if (node.attributes && node.attributes.res) {
162 | if (resKey) {
163 | res = node.attributes.res.find((r) => r.contentHandlerKey === this.name && r.key === resKey);
164 | } else {
165 | res = node.attributes.res.find((r) => r.contentHandlerKey === this.name && r.key === type);
166 | }
167 | }
168 |
169 | this._getResourceContentURL(node, type, resKey, parameters, res, (error, resourceContentURL) => {
170 | if (error) {
171 | return callback(error);
172 | }
173 |
174 | if (!resourceContentURL) {
175 | return callback("Invalid key '" + resKey + "'", true);
176 | }
177 |
178 | var ats = {
179 | contentURL: resourceContentURL
180 | };
181 |
182 | if (res) {
183 | ats.mtime = res.mtime;
184 | ats.mimeType = res.mimeType;
185 | ats.size = res.size;
186 | }
187 |
188 | this.service.sendContentURL(ats, request, response, callback);
189 | });
190 | }
191 | }
192 |
193 | module.exports = Abstract_Metas;
194 |
--------------------------------------------------------------------------------
/lib/contentHandlers/audio_musicmetadata.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const crypto = require('crypto');
5 |
6 | const mm = require('music-metadata');
7 | const Mime = require('mime');
8 |
9 | const debug = require('debug')('upnpserver:contentHandlers:Musicmetadata');
10 |
11 | const logger = require('../logger');
12 | const ContentHandler = require('./contentHandler');
13 |
14 | class Audio_MusicMetadata extends ContentHandler {
15 |
16 | /**
17 | *
18 | */
19 | get name () {
20 | return "musicMetadata";
21 | }
22 |
23 | /**
24 | *
25 | */
26 | prepareMetas (contentInfos, context, callback) {
27 |
28 | debug("Prepare", contentInfos);
29 |
30 | const contentURL = contentInfos.contentURL;
31 |
32 | let parsing = true;
33 |
34 | debug("Start musicMetadata contentURL=", contentURL);
35 |
36 | mm.parseFile(contentURL.path, {
37 | // duration : true
38 | // fileSize : stats.size
39 | }).then((tags) => {
40 |
41 | parsing = false;
42 |
43 | debug("Parsed musicMetadata path=", contentURL.path, "tags=", tags);
44 |
45 | if (!tags) {
46 | logger.error("MM does not support: " + contentURL.path);
47 | return callback();
48 | }
49 |
50 | const metas = {};
51 |
52 | ['title', 'album', 'duration'].forEach((n) => {
53 | if (tags[n]) {
54 | metas[n] = tags[n];
55 | }
56 | });
57 |
58 | metas.albumArtists = tags.common.albumartist ? [tags.common.albumartist] : tags.common.artists;
59 | metas.artists = tags.common.artists ? tags.common.artists : [tags.common.albumartist];
60 | metas.genres = tags.common.genre;
61 |
62 | if (tags.year) {
63 | metas.year = tags.year && parseInt(tags.year, 10);
64 | }
65 |
66 | var track = tags.track;
67 | if (track) {
68 | if (typeof (track.no) === "number" && track.no) {
69 | metas.originalTrackNumber = track.no;
70 |
71 | if (typeof (track.of) === "number" && track.of) {
72 | metas.trackOf = track.of;
73 | }
74 | }
75 | }
76 |
77 | var disk = tags.disk;
78 | if (disk) {
79 | if (typeof (disk.no) === "number" && disk.no) {
80 | metas.originalDiscNumber = disk.no;
81 |
82 | if (typeof (disk.of) === "number" && disk.of) {
83 | metas.diskOf = disk.of;
84 | }
85 | }
86 | }
87 |
88 | if (tags.picture) {
89 | var as = [];
90 | var res = [{}];
91 |
92 | var index = 0;
93 | tags.picture.forEach((picture) => {
94 | var mimeType = Mime.lookup(picture.format);
95 |
96 | var key = index++;
97 |
98 | if (!mimeType) {
99 | return;
100 | }
101 |
102 | if (!mimeType.indexOf("image/")) {
103 |
104 | var hash = computeHash(picture.data);
105 |
106 | as.push({
107 | contentHandlerKey: this.name,
108 | mimeType: mimeType,
109 | size: picture.data.length,
110 | hash: hash,
111 | key: key
112 | });
113 | return;
114 | }
115 |
116 | res.push({
117 | contentHandlerKey: this.name,
118 | mimeType: mimeType,
119 | size: picture.data.length,
120 | key: key
121 | });
122 |
123 | });
124 |
125 | if (as.length) {
126 | metas.albumArts = as;
127 | }
128 | if (res.length > 1) {
129 | metas.res = res;
130 | }
131 | }
132 |
133 | callback(null, metas);
134 | }).catch((err) => {
135 | logger.error("MM can not parse tags of contentURL=", contentURL, " error=",
136 | err);
137 | return callback();
138 | });
139 | }
140 |
141 | /**
142 | *
143 | */
144 | processRequest (node, request, response, path, parameters, callback) {
145 |
146 | var albumArtKey = parseInt(parameters[0], 10);
147 | if (isNaN(albumArtKey) || albumArtKey < 0) {
148 | let error = new Error("Invalid albumArtKey parameter (" + parameters + ")");
149 | error.node = node;
150 | error.request = request;
151 |
152 | return callback(error, false);
153 | }
154 |
155 | var contentURL = node.contentURL;
156 | // console.log("Get stream of " + node, node.attributes);
157 |
158 | this._getPicture(node, contentURL, albumArtKey, (error, picture) => {
159 |
160 | if (!picture.format || !picture.data) {
161 | let error = new Error('Invalid picture for node #' + node.id + ' key=' + albumArtKey);
162 | error.node = node;
163 | error.request = request;
164 |
165 | return callback(error, false);
166 | }
167 |
168 | response.setHeader("Content-Type", picture.format);
169 | response.setHeader("Content-Size", picture.data.length);
170 |
171 | response.end(picture.data, () => callback(null, true));
172 | });
173 | }
174 |
175 | _getPicture (node, contentURL, pictureIndex, callback) {
176 |
177 | contentURL.createReadStream(null, null, (error, stream) => {
178 | if (error) {
179 | return callback(error);
180 | }
181 |
182 | mm(stream, (error, tags) => {
183 | try {
184 | stream.destroy();
185 | } catch (x) {
186 | logger.error("Can not close stream", x);
187 | }
188 |
189 | if (error) {
190 | logger.error("Can not parse ID3 of " + contentURL, error);
191 | return callback("Can not parse ID3");
192 | }
193 |
194 | if (!tags || !tags.picture || tags.picture.length <= pictureIndex) {
195 | let error = new Error('Picture #' + pictureIndex + ' not found');
196 |
197 | logger.error(error);
198 | return callback(error);
199 | }
200 |
201 | var picture = tags.picture[pictureIndex];
202 | tags = null;
203 |
204 | callback(null, picture);
205 | });
206 | });
207 | }
208 | }
209 |
210 | function computeHash (buffer) {
211 | var shasum = crypto.createHash('sha1');
212 | shasum.update(buffer);
213 |
214 | return shasum.digest('hex');
215 | }
216 |
217 | module.exports = Audio_MusicMetadata;
218 |
--------------------------------------------------------------------------------
/lib/contentHandlers/contentHandler.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const fs = require('fs');
6 |
7 | const debug = require('debug')('upnpserver:contentHandler');
8 | const logger = require('../logger');
9 |
10 | class ContentHandler {
11 |
12 | constructor(configuration) {
13 | this._configuration = configuration || {};
14 | }
15 |
16 | get configuration() {
17 | return this._configuration;
18 | }
19 |
20 | /**
21 | *
22 | */
23 | get service() {
24 | return this._contentDirectoryService;
25 | }
26 |
27 | /**
28 | *
29 | */
30 | initialize(contentDirectoryService, callback) {
31 | this._contentDirectoryService = contentDirectoryService;
32 |
33 | var mimeTypes = this.mimeTypes;
34 | if (!mimeTypes) {
35 | return callback();
36 | }
37 |
38 | var prepareNode = (contentInfos, attributes, callback) => {
39 | debug("[", this.name, "] PrepareNode event of content", contentInfos);
40 |
41 | this.prepareMetasFromContentURL(contentInfos, attributes, (error) => {
42 | if (error) {
43 | logger.error("Prepare node " + contentInfos + " of contentHandler=" + this.name + " error=", error);
44 | return callback(error);
45 | }
46 |
47 | debug("[", this.name, "] PrepareNode event END of content", contentInfos);
48 | callback();
49 | });
50 | };
51 |
52 |
53 | var toJXML = (node, attributes, request, xml, callback) => {
54 |
55 | debug("[", this.name, "] toJXML event #", node.id);
56 |
57 | this.toJXML(node, attributes, request, xml, callback);
58 | };
59 |
60 |
61 | // Don't use => because we use arguments !
62 | var browse = (node, callback) => {
63 | debug("[", this.name, "] browse event #", node.id);
64 |
65 | this.browse(node, callback);
66 | };
67 |
68 | var priority = this.priority;
69 |
70 | mimeTypes.forEach((mimeType) => {
71 |
72 | if (this.prepareMetas) {
73 | debug("[", this.name, "] Register 'prepare' for mimeType", mimeType, "priority=", priority);
74 |
75 | contentDirectoryService.asyncOn("prepare:" + mimeType, prepareNode, priority);
76 | }
77 |
78 | if (this.toJXML) {
79 | debug("[", this.name, "] Register 'toJXML' for mimeType", mimeType, "priority=", priority);
80 |
81 | contentDirectoryService.asyncOn("toJXML:" + mimeType, toJXML, priority);
82 | }
83 |
84 | if (this.browse) {
85 | debug("[", this.name, "] Register 'browse' for mimeType", mimeType, "priority=", priority);
86 |
87 | contentDirectoryService.asyncOn("browse:" + mimeType, browse, priority);
88 | }
89 | });
90 |
91 | callback();
92 | }
93 |
94 | /*
95 | * prepareNode(node, callback) { callback(); }
96 | */
97 |
98 | searchUpnpClass(fileInfos, callback) {
99 | callback();
100 | }
101 |
102 | /**
103 | *
104 | */
105 | getResourceByParameter(node, parameter) {
106 | if (parameter instanceof Array) {
107 | parameter = parameter[0];
108 | }
109 |
110 | var res = node.attributes.res || [];
111 |
112 | debug("Find resource by parameter res=", res, "parameter=", parameter);
113 |
114 | return res.find((r) => r.key === parameter);
115 | }
116 |
117 | /**
118 | *
119 | */
120 | sendResource(contentURL, attributes, request, response, callback) {
121 | debug("[", this.name, "] Send resource contentURL=", contentURL, "attributes=", attributes);
122 |
123 | var opts = {};
124 | if (attributes._start) {
125 | opts.start = attributes._start;
126 | opts.end = opts.start + attributes.size - 1;
127 | }
128 |
129 | contentURL.createReadStream(null, opts, (error, stream) => {
130 | if (error) {
131 | logger.error('No stream for contentURL=', contentURL);
132 |
133 | if (!response.headersSent) {
134 | response.writeHead(404, 'Stream not found for linked content');
135 | }
136 | response.end();
137 | return callback(null, true);
138 | }
139 |
140 | if (attributes.mtime) {
141 | var m = attributes.mtime;
142 | if (typeof(m) === "number") {
143 | m = new Date(m);
144 | }
145 | response.setHeader('Last-Modified', m.toUTCString());
146 | }
147 | if (attributes.contentHash) {
148 | response.setHeader('ETag', attributes.hash);
149 | }
150 | response.setHeader('Content-Length', attributes.size);
151 | if (attributes.mimeType !== undefined) {
152 | response.setHeader('Content-Type', "image/jpeg"); //attributes.mimeType);
153 | }
154 |
155 | stream.pipe(response);
156 |
157 | stream.on('end', () => callback(null, true));
158 | });
159 |
160 | }
161 |
162 | /**
163 | *
164 | */
165 | _mergeMetas(attributes, metas) {
166 |
167 | debug("Merge metas=", metas, "to attributes=", attributes);
168 | if (!metas) {
169 | return attributes;
170 | }
171 |
172 | var copyRes = (index, datas) => {
173 | attributes.res = attributes.res || [];
174 |
175 | var r = attributes.res[index];
176 | if (!r) {
177 | r = {};
178 | attributes.res[index] = r;
179 | }
180 |
181 | for (var n in datas) {
182 | r[n] = datas[n];
183 | }
184 | };
185 |
186 | for (var n in metas) {
187 | var m = metas[n];
188 | if (n === 'res') {
189 | for (var i = 0; i < m.length; i++) {
190 | copyRes(i, m[i]);
191 | }
192 | continue;
193 | }
194 |
195 | var c = attributes[n];
196 | /*if (false) {
197 | // Merge artists, albums ??? (a good or bad idea ?)
198 | if (Array.isArray(c) && Array.isArray(m)) {
199 | m.forEach((tok) => {
200 | if (c.indexOf(tok)>=0) {
201 | return;
202 | }
203 | c.push(tok);
204 | });
205 | }
206 | }*/
207 |
208 | if (c) {
209 | return;
210 | }
211 |
212 | attributes[n] = m;
213 | }
214 |
215 | return attributes;
216 | }
217 |
218 | /**
219 | *
220 | */
221 | prepareMetasFromContentURL(contentInfos, attributes, callback) {
222 | if (!this.prepareMetas) {
223 | return callback(null, attributes);
224 | }
225 |
226 | this.prepareMetas(contentInfos, attributes, (error, metas) => {
227 | if (error) {
228 | logger.error("loadMetas error", contentInfos, error);
229 | // return callback(error); // Continue processing ...
230 | }
231 |
232 | attributes = this._mergeMetas(attributes, metas);
233 |
234 | callback(null, attributes);
235 | });
236 | }
237 | }
238 |
239 | module.exports = ContentHandler;
240 |
--------------------------------------------------------------------------------
/lib/contentHandlers/exif.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6, maxlen: 180 */
2 | "use strict";
3 |
4 | const debug = require('debug')('upnpserver:contentHandlers:Exif');
5 | const Exif = require('exif');
6 |
7 | const ContentHandler = require('./contentHandler');
8 |
9 | const logger = require('../logger');
10 |
11 | class ExifContentHandler extends ContentHandler {
12 |
13 | get name() {
14 | return "exif";
15 | }
16 |
17 | /**
18 | *
19 | */
20 | prepareMetas(contentInfos, context, callback) {
21 | debug("Prepare metas of", contentInfos);
22 |
23 | var contentURL = contentInfos.contentURL;
24 |
25 | contentURL.readContent(null, (error, imageData) => {
26 | if (error) {
27 | logger.error("Can not get content of '" + contentURL + "'", error);
28 | return callback(error);
29 | }
30 |
31 | new Exif.ExifImage({image: imageData, fixThumbnailOffset: true}, (error, exifData) => {
32 | if (error) {
33 | logger.error("Can not parse exif '" + contentURL + "'", error);
34 | return callback(error);
35 | }
36 |
37 | filter(exifData);
38 |
39 | var res;
40 | var thumbnail = exifData.thumbnail;
41 | if (thumbnail) {
42 | delete exifData.thumbnail;
43 |
44 | if (thumbnail.ThumbnailOffset && thumbnail.ThumbnailLength) {
45 |
46 | var thumbnailMimeType;
47 | if (isJPEG(imageData, thumbnail.ThumbnailOffset)) {
48 | thumbnailMimeType = "image/jpeg";
49 | }
50 |
51 | if (thumbnailMimeType) {
52 | res = res || [{}];
53 |
54 | res.push({
55 | contentHandlerKey: this.name,
56 | key: "thumbnail",
57 | mimeType: thumbnailMimeType,
58 | size: thumbnail.ThumbnailLength,
59 | _start: thumbnail.ThumbnailOffset
60 | });
61 | }
62 | }
63 | }
64 | var exif = exifData;
65 | if (exif) {
66 | res = res || [{}];
67 |
68 | if (exif.ExifImageWidth) {
69 | res[0].width = exif.ExifImageWidth;
70 | }
71 | if (exif.ExifImageHeight) {
72 | res[0].height = exif.ExifImageHeight;
73 | }
74 | if (exif.DateTimeOriginal) {
75 | var ds = exif.DateTimeOriginal;
76 | var reg = /^(\d{4}).(\d{2}).(\d{2}).(\d{2}).(\d{2}).(\d{2})/.exec(ds);
77 | if (reg) {
78 | exifData.dateTimeOriginal = new Date(parseInt(reg[1]), parseInt(reg[2]) - 1, parseInt(reg[3]),
79 | parseInt(reg[4]), parseInt(reg[5]), parseInt(reg[6]));
80 | exifData.date = exifData.dateTimeOriginal;
81 | }
82 | }
83 | }
84 |
85 | if (res) {
86 | exifData.res = res;
87 | }
88 |
89 | callback(null, exifData);
90 | });
91 | });
92 | }
93 |
94 | /**
95 | *
96 | */
97 | processRequest(node, request, response, path, parameters, callback) {
98 |
99 | var resKey = parameters[0];
100 | var res = this.getResourceByParameter(node, resKey);
101 |
102 | debug("ProcessRequest", "resKey=", resKey, "=>", res);
103 |
104 | if (!res) {
105 | var error = new Error("Invalid resKey parameter (" + resKey + ")");
106 | return callback(error, true);
107 | }
108 |
109 | this.sendResource(node.contentURL, res, request, response, callback);
110 | }
111 | }
112 |
113 | module.exports = ExifContentHandler;
114 |
115 | function isJPEG(data, offset) {
116 | debug("IsJPEG: offset=", offset, "data=", data[offset], data[offset + 1], data[offset + 2]);
117 | return (data[offset] === 0xff) && (data[offset + 1] === 0xd8) && (data[offset + 2] === 0xff) && (data[offset + 3] === 0xdb);
118 | }
119 |
120 | function filter(json) {
121 | for (var n in json) {
122 | var v = json[n];
123 | if (typeof(v) !== "object") {
124 | continue;
125 | }
126 |
127 | if (v === null) {
128 | continue;
129 | }
130 | if (v instanceof Array) {
131 | continue;
132 | }
133 | if (Buffer.isBuffer(v)) {
134 | delete json[n];
135 | continue;
136 | }
137 | if (v instanceof Date) {
138 | continue;
139 | }
140 | if (!Object.keys(v).length) {
141 | delete json[n];
142 | continue;
143 | }
144 |
145 | filter(v);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/lib/contentHandlers/ffprobe.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const spawn = require('child_process').spawn;
5 |
6 | const debug = require('debug')('upnpserver:contentHandlers:FFprobe');
7 | const debugData = require('debug')('upnpserver:contentHandlers:FFprobe:data');
8 | const logger = require('../logger');
9 |
10 | const Abstract_Metas = require('./abstract_metas');
11 |
12 | class ffprobe extends Abstract_Metas {
13 | constructor(configuration) {
14 | super(configuration);
15 |
16 | var ffprobe = this._configuration.ffprobe_path;
17 | if (!ffprobe) {
18 | ffprobe = process.env.FFPROBE_PATH;
19 |
20 | if (!ffprobe) {
21 | // ffprobe = "ffprobe";
22 | }
23 | }
24 |
25 | this.ffprobe_path = ffprobe;
26 |
27 | debug("ffprobe", "set EXE path=", this.ffprobe_path);
28 | }
29 |
30 | get name() {
31 | return "ffprobe";
32 | }
33 |
34 | /**
35 | *
36 | */
37 | prepareMetas(contentInfos, context, callback) {
38 | if (!this.ffprobe_path) {
39 | return callback();
40 | }
41 |
42 | var contentURL = contentInfos.contentURL;
43 |
44 | var localPath = '-';
45 |
46 | if (contentURL.contentProvider.isLocalFilesystem) {
47 | localPath = contentURL;
48 | }
49 |
50 | // TODO use ContentProvider
51 | var parameters = ['-show_streams', '-show_format', '-print_format', 'json',
52 | '-loglevel', 'warning', localPath];
53 |
54 | debug("prepareMetas", "Launch ffprobe", this.ffprobe_path, "parameters=", parameters, "localPath=", localPath);
55 |
56 | var proc = spawn(this.ffprobe_path, parameters);
57 | var probeData = [];
58 | var errData = [];
59 | var exitCode;
60 | var start = Date.now();
61 |
62 | var callbackCalled = false;
63 |
64 | proc.stdout.setEncoding('utf8');
65 | proc.stderr.setEncoding('utf8');
66 |
67 | proc.stdout.on('data', (data) => {
68 | debugData("prepareMetas", "receive stdout=", data);
69 | probeData.push(data);
70 | });
71 | proc.stderr.on('data', (data) => {
72 | debugData("prepareMetas", "receive stderr=", data);
73 | errData.push(data);
74 | });
75 |
76 | proc.on('exit', (code) => {
77 | debug("prepareMetas", "Exit event received code=", code);
78 | exitCode = code;
79 | });
80 | proc.on('error', (error) => {
81 | debug("prepareMetas", "Error event received error=", error, "callbackCalled=", callbackCalled);
82 |
83 | if (error) {
84 | logger.error("parseURL", contentURL, error);
85 | }
86 |
87 | if (callbackCalled) {
88 | return;
89 | }
90 | callbackCalled = true;
91 | callback();
92 | });
93 |
94 | proc.on('close', () => {
95 | debug("prepareMetas", "Close event received exitCode=", exitCode, "callbackCalled=", callbackCalled);
96 | debugData("prepareMetas", "probeData=", probeData);
97 | debugData("prepareMetas", "errData=", errData);
98 |
99 | if (callbackCalled) {
100 | return;
101 | }
102 | callbackCalled = true;
103 |
104 | if (!probeData) {
105 | setImmediate(callback);
106 | return;
107 | }
108 |
109 | if (exitCode) {
110 | var err_output = errData.join('');
111 |
112 | var error = new Error("FFProbe error: " + err_output);
113 | logger.error(error);
114 | return callback(error);
115 | }
116 |
117 | var json = JSON.parse(probeData.join(''));
118 | json.probe_time = Date.now() - start;
119 |
120 | try {
121 | this._processProbe(json, callback);
122 |
123 | } catch (x) {
124 | logger.error(x);
125 | }
126 | });
127 |
128 | if (localPath === '-') {
129 | debug("prepareMetas", "Read stream", contentURL, "...");
130 | contentURL.createReadStream(null, null, (error, stream) => {
131 |
132 | if (error) {
133 | logger.error("Can not get stream of '" + contentURL + "'", error);
134 | return callback(error);
135 | }
136 |
137 | debug("prepareMetas", "Pipe stream", contentURL, " to ffprobe");
138 |
139 | stream.pipe(proc.stdin);
140 | });
141 | }
142 | }
143 |
144 | /**
145 | *
146 | */
147 | _processProbe(json, callback) {
148 | debug("_processProbe", "Process json=", json);
149 |
150 | var video = false;
151 | var audio = false;
152 |
153 | var res = {};
154 |
155 | var components = [];
156 |
157 | var componentInfos = [{
158 | groupId: 0,
159 | components: components
160 | }];
161 |
162 | var streams = json.streams;
163 | if (streams.length) {
164 | streams.forEach((stream) => {
165 |
166 | if (stream.codec_type === "video") {
167 |
168 | var component = {
169 | componentID: "video_" + components.length,
170 | componentClass: "Video"
171 | };
172 | components.push(component);
173 |
174 | switch (stream.codec_name) {
175 | case "mpeg1video":
176 | component.mimeType = "video/mpeg";
177 | break;
178 | case "mpeg4":
179 | component.mimeType = "video/mpeg4"; // MPEG-4 part 4
180 | break;
181 | case "h261":
182 | component.mimeType = "video/h261";
183 | break;
184 | case "h263":
185 | component.mimeType = "video/h263";
186 | break;
187 | case "h264":
188 | component.mimeType = "video/h264";
189 | break;
190 | case "hevc":
191 | component.mimeType = "video/hevc";
192 | break;
193 | case "vorbis":
194 | component.mimeType = "video/ogg";
195 | break;
196 | }
197 | var tags = stream.tags;
198 | if (tags) {
199 | if (tags.title) {
200 | component.title = tags.title;
201 | }
202 | }
203 |
204 | if (!video) {
205 | video = true;
206 |
207 | if (stream.width && stream.height) {
208 | res.resolution = stream.width + "x" + stream.height;
209 | }
210 |
211 | if (stream.duration) {
212 | res.duration = parseFloat(stream.duration);
213 | }
214 | if (stream.codec_name) {
215 | res.vcodec = stream.codec_name;
216 | }
217 | }
218 | return;
219 | }
220 | if (stream.codec_type === "audio") {
221 | let component = {
222 | componentID: "audio_" + components.length,
223 | componentClass: "Audio"
224 | };
225 |
226 | switch (stream.codec_name) {
227 | case "mp2":
228 | component.mimeType = "audio/mpeg";
229 | break;
230 | case "mp4":
231 | component.mimeType = "audio/mpeg4";
232 | break;
233 | case "dca":
234 | component.mimeType = "audio/dca"; // DTS
235 | break;
236 | case "aac":
237 | component.mimeType = "audio/ac3"; // Dolby
238 | break;
239 | case "aac":
240 | component.mimeType = "audio/aac";
241 | break;
242 | case "webm":
243 | component.mimeType = "audio/webm";
244 | break;
245 | case "wav":
246 | component.mimeType = "audio/wave";
247 | break;
248 | case "flac":
249 | component.mimeType = "audio/flac";
250 | break;
251 | case "vorbis":
252 | component.mimeType = "audio/ogg";
253 | break;
254 | }
255 | if (stream.channels) {
256 | component.nrAudioChannels = stream.channels;
257 | }
258 |
259 | let tags = stream.tags;
260 | if (tags) {
261 | if (tags.language) {
262 | component.language = convertLanguage(tags.language);
263 | }
264 | if (tags.title) {
265 | component.title = tags.title;
266 | }
267 | }
268 |
269 | if (!audio) {
270 | audio = true;
271 |
272 | if (stream.duration) {
273 | res.duration = parseFloat(stream.duration);
274 | }
275 | if (stream.codec_name) {
276 | res.acodec = stream.codec_name;
277 | }
278 | if (stream.bit_rate) {
279 | res.bitrate = stream.bit_rate;
280 | }
281 | if (stream.channels) {
282 | res.nrAudioChannels = stream.channels;
283 | }
284 | if (stream.sample_rate) {
285 | res.sampleFrequency = stream.sample_rate;
286 | }
287 | if (stream.bit_rate) {
288 | res.bitrate = stream.bit_rate;
289 | }
290 | }
291 | return;
292 | }
293 | if (stream.codec_type === "subtitle") {
294 | let component = {
295 | componentID: "sub_" + components.length,
296 | componentClass: "Subtitle"
297 | };
298 |
299 | let tags = stream.tags;
300 | if (tags) {
301 | if (tags.language) {
302 | component.language = convertLanguage(tags.language);
303 | }
304 | if (tags.title) {
305 | component.title = tags.title;
306 | }
307 | }
308 | }
309 | });
310 |
311 | // One property ?
312 | if (components.length) {
313 | res.componentInfos = componentInfos;
314 | }
315 | }
316 |
317 | debug("_processProbe", "FFProbe res=", res);
318 |
319 | var metas = {
320 | res: [res]
321 | };
322 |
323 | callback(null, metas);
324 | }
325 | }
326 |
327 | function convertLanguage(lang) {
328 | return lang;
329 | }
330 |
331 | module.exports = ffprobe;
332 |
--------------------------------------------------------------------------------
/lib/contentHandlers/metas.images.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6, maxlen: 180 */
2 | "use strict";
3 |
4 | const Path = require('path');
5 | const Async = require('async');
6 | const Mime = require('mime');
7 |
8 | const debug = require('debug')('upnpserver:contentHandlers:MetasImages');
9 | const logger = require('../logger');
10 |
11 | const MetasJson = require('./metas.json');
12 |
13 | const gm = require('gm');
14 |
15 | class MetasImages extends MetasJson {
16 | constructor(configuration) {
17 | super(configuration);
18 | }
19 |
20 | _tryDownloadImageURL(url, callback) {
21 | callback(null, false);
22 | }
23 |
24 | _addImage(metas, imageURL, imagePath, suggestedWidth, suggestedHeight, key, index, resizeWidths, isBaseURL, callback) {
25 | debug("_addImage", "imageURL=", imageURL, "key=", key, "index=", index);
26 |
27 | var resKey = key + "/" + (index + 1);
28 |
29 | var resize = (stats, w, h, maxw, maxh) => {
30 | if (w > maxw) {
31 | var d = maxw / w;
32 | w = Math.floor(d * w);
33 | h = Math.floor(d * h);
34 | }
35 |
36 | if (h > maxh) {
37 | var d2 = maxh / h;
38 | w = Math.floor(d2 * w);
39 | h = Math.floor(d2 * h);
40 | }
41 |
42 | var i = {
43 | contentHandlerKey: this.name,
44 | key: resKey,
45 | paramURL: "w" + maxw,
46 | additionalInfo: "type=" + key,
47 | width: w,
48 | height: h,
49 | imagePath: imagePath
50 | };
51 |
52 | if (stats) {
53 | i.mimeType = stats.mimeType;
54 | i.mtime = stats.mtime.getTime();
55 | } else {
56 | var mt = Mime.lookup(imagePath);
57 | if (mt) {
58 | i.mimeType = mt;
59 | }
60 | }
61 | if (isBaseURL) {
62 | i.baseURL = true;
63 | }
64 |
65 | metas.res.push(i);
66 | };
67 |
68 | var addSizes = (stats, w, h) => {
69 | metas.res = metas.res || [{}];
70 | var i = {
71 | contentHandlerKey: this.name,
72 | key: resKey,
73 | additionalInfo: "type=" + key,
74 | width: w,
75 | height: h,
76 | imagePath: imagePath
77 | };
78 |
79 | if (stats) {
80 | i.mimeType = stats.mimeType;
81 | i.mtime = stats.mtime.getTime();
82 | i.size = stats.size;
83 | } else {
84 | var mt = Mime.lookup(imagePath);
85 | if (mt) {
86 | i.mimeType = mt;
87 | }
88 | }
89 | if (isBaseURL) {
90 | i.baseURL = true;
91 | }
92 |
93 | metas.res.push(i);
94 |
95 | if (!resizeWidths) {
96 | return;
97 | }
98 |
99 | if (w > 4096 || h > 4096) {
100 | resize(stats, w, h, 4096, 4096);
101 | }
102 |
103 | if (w > 1024 || h > 768) {
104 | resize(stats, w, h, 1024, 768);
105 | }
106 |
107 | if (w > 640 || h > 480) {
108 | resize(stats, w, h, 640, 480);
109 | }
110 |
111 | if (w > 160 || h > 160) {
112 | resize(stats, w, h, 160, 160);
113 | }
114 | };
115 |
116 | imageURL.stat((error, stats) => {
117 | if (error || !stats) {
118 | debug("_addImage", "Can not locate imageURL", imageURL, error);
119 |
120 | // tmdb does not load all images ... try to download it !
121 |
122 | /*
123 | this._tryDownloadImageURL(imageURL, (error, stats) => {
124 | if (error) {
125 | console.error("Can not download image ",imageURL,"error=",error);
126 | return callback();
127 | }
128 |
129 | if (!stats) {
130 | return callback();
131 | }
132 |
133 | addSizes(stats, suggestedWidth, suggestedWidth);
134 | callback();
135 | });
136 | */
137 | addSizes(null, suggestedWidth, suggestedHeight);
138 | return callback();
139 | }
140 |
141 | if (suggestedWidth && suggestedHeight) {
142 | addSizes(stats, suggestedWidth, suggestedHeight);
143 | return callback();
144 | }
145 |
146 | var session = null; // {}; // Does not work with gm ?
147 | imageURL.createReadStream(session, {}, (error, stream) => {
148 | if (error) {
149 | return callback(error);
150 | }
151 |
152 | gm(stream).identify((error, gmJson) => {
153 | if (error) {
154 | imageURL.contentProvider.end(session, (error2) => {
155 | callback(error || error2);
156 | });
157 |
158 | return;
159 | }
160 |
161 | // debug("_addImage", "Image json=",json);
162 |
163 | var w = gmJson.size.width;
164 | var h = gmJson.size.height;
165 |
166 | addSizes(stats, w, h);
167 | callback();
168 | });
169 | });
170 | });
171 | }
172 |
173 | _convertImageSize(session, imageURL, originalStats, originalSizes, sizeSuffix, width, height, callback) {
174 | debug("_convertImageSize", "imageURL=", imageURL, "width=", width, "height=", height);
175 |
176 | if (!originalSizes) {
177 | imageURL.createReadStream(null, {}, (error, stream) => {
178 | if (error) {
179 | return callback(error);
180 | }
181 |
182 | gm(stream).identify((error, gmJson) => {
183 | if (error) {
184 | return callback(error);
185 | }
186 |
187 | var sizes = {
188 | width: gmJson.size.width,
189 | height: gmJson.size.height
190 | };
191 | this._convertImage(session, imageURL, originalStats, sizes, sizeSuffix, width, height, callback);
192 | });
193 | });
194 | return;
195 | }
196 |
197 |
198 | var reg = /(.*)\.([^.]+)$/.exec(imageURL.basename);
199 | if (!reg) {
200 | logger.error("Can not parse '" + imageURL + "'");
201 | return callback("Path problem");
202 | }
203 |
204 | var newBasename = reg[1] + sizeSuffix + '.' + reg[2];
205 |
206 | var imageURL2 = imageURL.changeBasename(newBasename);
207 |
208 | debug("_convertImageSize", "imageURL", imageURL, "=> imageURL2=", imageURL2);
209 |
210 | imageURL2.stat((error, stats2) => {
211 | if (!error && stats2 && stats2.size > 0) {
212 | debug("_convertImageSize", "date=", originalStats.mtime, "date2=", stats2.mtime);
213 |
214 | if (stats2.mtime.getTime() > originalStats.mtime.getTime()) {
215 | imageURL2.createReadStream(null, {}, (error, stream2) => {
216 | if (error) {
217 | return callback(error);
218 | }
219 | gm(stream2).identify((error, json) => {
220 | if (error) {
221 | return callback(error);
222 | }
223 |
224 | callback(null, imageURL2, stats2, json);
225 | });
226 | });
227 | return;
228 | }
229 | }
230 |
231 | imageURL.createReadStream(null, {}, (error, stream) => {
232 | if (error) {
233 | return callback(error);
234 | }
235 |
236 | imageURL2.createWriteStream({}, (error, writeStream) => {
237 | if (error) {
238 | return callback(error);
239 | }
240 |
241 | var w = originalSizes.width;
242 | var h = originalSizes.height;
243 |
244 | if (w > width) {
245 | var d = width / w;
246 | w = Math.floor(d * w);
247 | h = Math.floor(d * h);
248 | }
249 |
250 | if (h > height) {
251 | var d2 = height / h;
252 | w = Math.floor(d2 * w);
253 | h = Math.floor(d2 * h);
254 | }
255 |
256 | debug("_convertImageSize", "Resize image width=", w, "height=", h);
257 |
258 | writeStream.on('finish', () => {
259 | debug("_convertImageSize", "Catch end message");
260 |
261 | imageURL2.stat((error, stats2) => {
262 | debug("_convertImageSize", "Stat2=",stats2,"error=",error);
263 | if (error) {
264 | return callback(error);
265 | }
266 |
267 | imageURL2.createReadStream(null, {}, (error, stream2) => {
268 | debug("_convertImageSize", "Create read stream error=",error);
269 |
270 | if (error) {
271 | return callback(error);
272 | }
273 |
274 | gm(stream2).identify((error, json2) => {
275 | debug("_convertImageSize", "Identify2 json2=", json2, "error=",error);
276 |
277 | if (error) {
278 | return callback(error);
279 | }
280 |
281 | callback(null, imageURL2, stats2, json2);
282 | });
283 | });
284 | });
285 | });
286 |
287 | writeStream.on('error', (error) => {
288 | debug("_convertImageSize", "Catch error message", error);
289 | callback(error);
290 | });
291 |
292 | gm(stream).resize(w, h).stream().pipe(writeStream);
293 | });
294 | });
295 | });
296 | }
297 | }
298 |
299 | module.exports = MetasImages;
300 |
--------------------------------------------------------------------------------
/lib/contentHandlers/omdb.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const spawn = require('child_process').spawn;
5 |
6 | const debug = require('debug')('upnpserver:contentHandlers:FFprobe');
7 | const logger = require('../logger');
8 |
9 | const Abstract_Metas = require('./abstract_metas');
10 |
11 | class omdb extends Abstract_Metas {
12 | constructor(configuration) {
13 | super(configuration);
14 | }
15 |
16 | get name() {
17 | return "omdb";
18 | }
19 |
20 | /**
21 | *
22 | */
23 | prepareMetas(contentInfos, context, callback) {
24 | }
25 | }
26 |
27 | module.exports = omdb;
28 |
--------------------------------------------------------------------------------
/lib/contentHandlers/srt.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debug = require('debug')('upnpserver:contentHandlers.Srt');
5 | const logger = require('../logger');
6 |
7 | const ContentHandler = require('./contentHandler');
8 |
9 | class Srt extends ContentHandler {
10 |
11 | get name() {
12 | return "srt";
13 | }
14 |
15 | /**
16 | *
17 | */
18 | prepareMetas(contentInfos, context, callback) {
19 |
20 | var contentURL = contentInfos.contentURL;
21 |
22 | var srtBasename = contentURL.basename.replace(/\.[^.]*$/, '.srt'); // TODO Don't use replace !
23 |
24 | var srtURL = contentURL.changeBasename(srtBasename);
25 |
26 | srtURL.stat((error, stats) => {
27 | if (error) {
28 | if (error.code === "ENOENT") {
29 | return callback();
30 | }
31 |
32 | return callback(error);
33 | }
34 |
35 | if (stats.isFile() && stats.size > 0) {
36 | debug("prepareMetas", "SRT detected => url=" + srtURL);
37 |
38 | var res = [{}];
39 | var metas = {
40 | res: res
41 | };
42 |
43 | res.push({
44 | contentHandlerKey: this.name,
45 | key: "1",
46 | type: "srt",
47 | mimeType: "text/srt",
48 | size: stats.size,
49 | mtime: stats.mtime.getTime()
50 | });
51 |
52 | return callback(null, metas);
53 | }
54 |
55 | return callback();
56 | });
57 | }
58 |
59 | /**
60 | *
61 | */
62 | processRequest(node, request, response, path, parameters, callback) {
63 |
64 | var contentURL = node.contentURL;
65 | var basename = contentURL.basename;
66 | var srtURL = contentURL.changeBasename(basename.replace(/\.[^.]*$/, '.srt'));
67 |
68 | debug("processRequest", "srtURL=", srtURL, "contentURL=", contentURL);
69 |
70 | this.service.sendContentURL({
71 | contentURL: srtURL
72 |
73 | }, request, response, callback);
74 | }
75 | }
76 |
77 | module.exports = Srt;
78 |
--------------------------------------------------------------------------------
/lib/contentHandlers/video_matroska.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, nomen: true, esversion: 6 */
2 | "use strict";
3 |
4 | const matroska = require('matroska');
5 | const Path = require('path');
6 |
7 | const debug = require('debug')('upnpserver:contentHandlers:Matroska');
8 | const logger = require('../logger');
9 |
10 | const ContentHandler = require('./contentHandler');
11 |
12 | class Video_Matroska extends ContentHandler {
13 |
14 | /**
15 | *
16 | */
17 | get name() {
18 | return "matroska";
19 | }
20 |
21 | /**
22 | *
23 | */
24 | prepareMetas(contentInfos, context, callback) {
25 |
26 | var contentURL = contentInfos.contentURL;
27 |
28 | var d1 = 0;
29 | if (debug.enabled) {
30 | debug("prepareMetas", "Parse matroska", contentURL);
31 | d1 = Date.now();
32 | }
33 |
34 | var parsing;
35 |
36 | try {
37 | parsing = true;
38 |
39 | var source = new matroska.StreamFactorySource({
40 | getStream(session, options, callback) {
41 | // console.log("getstream", options);
42 |
43 | debug("prepareMetas", "getStream session=", session, " options=", options);
44 |
45 | contentURL.createReadStream(session, options, callback);
46 | },
47 | end(session, callback) {
48 | debug("prepareMetas", "endStream session=", session);
49 |
50 | contentURL.contentProvider.end(session, callback);
51 | }
52 | });
53 |
54 | matroska.Decoder.parseInfoTagsAndAttachments(source, (error, document) => {
55 |
56 | parsing = false;
57 |
58 | d1 = Date.now() - d1;
59 |
60 | debug("prepareMetas", "Matroska parsed [", d1, "ms] contentURL=", contentURL, "error=", error);
61 |
62 | // debug("Return ", attributes.contentURL, error, document);
63 |
64 | if (error || !document) {
65 | logger.error("Can not parse mkv " + contentURL, error);
66 | return callback();
67 | }
68 |
69 | if (debug.enabled) {
70 | debug(document.print());
71 | }
72 |
73 | var segment = document.firstSegment;
74 | if (!segment) {
75 | return callback();
76 | }
77 |
78 | var metas = {};
79 |
80 | var info = segment.info;
81 | try {
82 | if (info) {
83 | if (info.title) {
84 | metas.title = info.title;
85 | }
86 | }
87 | } catch (x) {
88 | // Some matroska error
89 | console.error(error);
90 | }
91 |
92 | // console.log(contentURL + "=>" + document.print());
93 |
94 | var tags = segment.tags;
95 |
96 | var attachments = segment.attachments;
97 | if (attachments && attachments.attachedFiles) {
98 |
99 | var res = [{}];
100 |
101 | attachments.attachedFiles.forEach((attachedFile) => {
102 |
103 | var fileData = attachedFile.$$fileData;
104 | if (!fileData) {
105 | return;
106 | }
107 |
108 | var name = attachedFile.fileName;
109 | var ret = /([^\.]*)\..*/.exec(name);
110 | if (ret) {
111 | name = ret[1];
112 | }
113 |
114 | var mimeType = attachedFile.fileMimeType;
115 | var png = (mimeType === "image/png");
116 |
117 | var r = {
118 | contentHandlerKey: this.name,
119 | key: attachedFile.fileUID,
120 | mimeType: mimeType,
121 | size: fileData.getDataSize()
122 | // mtime: stats.mtime.getTime()
123 | };
124 |
125 | debug("prepareMetas", "Attachment:", name, r);
126 |
127 | switch (name) {
128 | case 'cover':
129 | case 'cover_land':
130 | r.dlnaProfile = (png) ? "PNG_MED" : "JPEG_MED";
131 | res.push(r);
132 | break;
133 |
134 | case 'small_cover':
135 | case 'small_cover_land':
136 | r.dlnaProfile = (png) ? "PNG_TN" : "JPEG_TN";
137 | res.push(r);
138 | break;
139 | }
140 | });
141 |
142 | if (res.length > 1) {
143 | metas.res = res;
144 | }
145 | }
146 |
147 | callback(null, metas);
148 | });
149 |
150 | } catch (x) {
151 | if (parsing) {
152 | logger.error("MKV: Parsing exception" + contentURL, x);
153 |
154 | return callback();
155 | }
156 |
157 | throw x;
158 | }
159 | }
160 |
161 | /**
162 | *
163 | */
164 | processRequest(node, request, response, path, parameters, callback) {
165 |
166 | var resKey = parseFloat(parameters[0]);
167 |
168 | debug("processRequest", "Process request ", resKey);
169 |
170 | if (isNaN(resKey)) {
171 | var error = new Error("Invalid resKey parameter (" + resKey + ")");
172 | return callback(error, true);
173 | }
174 |
175 | var attributes = node.attributes;
176 |
177 | var contentURL = node.contentURL;
178 |
179 | var source = new matroska.StreamFactorySource({
180 | getStream(session, options, callback) {
181 | debug("processRequest", "getStream session=", session, "options=", options);
182 |
183 | contentURL.createReadStream(session, options, callback);
184 | },
185 | end(session, callback) {
186 | debug("processRequest", "endStream session=", session);
187 |
188 | contentURL.contentProvider.end(session, callback);
189 | }
190 | });
191 |
192 | matroska.Decoder.parseInfoTagsAndAttachments(source, (error, document) => {
193 |
194 | if (error || !document) {
195 | return callback(error);
196 | }
197 |
198 | var segment = document.firstSegment;
199 | if (!segment) {
200 | return callback(new Error("No segment"), true);
201 | }
202 | var attachments = segment.attachments;
203 | if (!attachments) {
204 | return callback(new Error("No attachments"), true);
205 | }
206 |
207 | var attachedFile = attachments.attachedFiles.find((a) => a.fileUID === resKey);
208 |
209 | if (!attachedFile) {
210 | return callback(new Error("Can not find resource '" + resKey + "'"), true);
211 | }
212 |
213 | var fileData = attachedFile.$$fileData;
214 |
215 | var fileMimeType = attachedFile.fileMimeType;
216 | if (fileMimeType) {
217 | response.setHeader("Content-Type", fileMimeType);
218 | }
219 | response.setHeader("Content-Size", fileData.getDataSize());
220 | if (node.contentTime) {
221 | var mtime = new Date(node.contentTime);
222 | response.setHeader("Last-Modified", mtime.toUTCString());
223 | }
224 |
225 | fileData.getDataStream((error, stream) => {
226 | if (error) {
227 | return callback(error, true);
228 | }
229 |
230 | stream.pipe(response);
231 |
232 | stream.on('end', () => callback(null, true));
233 | });
234 | });
235 | }
236 | }
237 |
238 | module.exports = Video_Matroska;
239 |
--------------------------------------------------------------------------------
/lib/contentProviders/contentProvider.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debugHash = require('debug')('upnpserver:contentProvider:hash');
5 | const crypto = require('crypto');
6 | const Path = require('path');
7 |
8 | const URL = require('../util/url');
9 |
10 | const HASH_SIZE = 1024 * 1024;
11 |
12 | const CHANGE_PATH_SEPARATOR = (Path.sep !== '/');
13 |
14 | class ContentProvider {
15 | /**
16 | *
17 | */
18 | constructor(configuration) {
19 | this._configuration = configuration || {};
20 | }
21 |
22 | /**
23 | *
24 | */
25 | initialize(service, callback) {
26 | this._service=service;
27 | callback(null);
28 | }
29 |
30 | /**
31 | *
32 | */
33 | normalizeParameter(value) {
34 | var replaced = value.replace(/\$\{([^\}]+)\}/g, function(_,name) {
35 | return process.env[name] || "";
36 | });
37 |
38 | return replaced;
39 | }
40 |
41 | /**
42 | *
43 | */
44 | get service() {
45 | return this._service;
46 | }
47 |
48 |
49 | /**
50 | *
51 | */
52 | get isLocalFilesystem() {
53 | return false;
54 | }
55 |
56 | /*
57 | * CAUTION !!!! This function must return a list of COMPLETE URL, not only the filename
58 | *
59 | */
60 | readdir(url, callback) {
61 | callback(new Error("not supported (url=" + url + ")"));
62 | }
63 |
64 | /*
65 | * CAUTION !!!! Stat must return an object with a field 'mime' which specifies the mime type of the resource
66 | *
67 | */
68 | stat(url, callback) {
69 | callback(new Error("not supported (url=" + url + ")"));
70 | }
71 |
72 | /**
73 | *
74 | */
75 | createReadStream(session, url, options, callback) {
76 | callback(new Error("not supported (url=" + url + ")"));
77 | }
78 |
79 | /**
80 | *
81 | */
82 | createWriteStream(url, options, callback) {
83 | callback(new Error("not supported (url=" + url + ")"));
84 | }
85 |
86 | /**
87 | *
88 | */
89 | end(session, callback) {
90 | callback();
91 | }
92 |
93 | /**
94 | * Join paths
95 | *
96 | * @see Path.join
97 | */
98 | join(path, newPath) {
99 | var p = Path.posix.join.apply(Path, arguments);
100 |
101 | return p;
102 | }
103 |
104 | /**
105 | *
106 | */
107 | readContent(uri, encoding, callback) {
108 | if (arguments.length==2) {
109 | callback=encoding;
110 | encoding=undefined;
111 | }
112 |
113 | var ps={
114 | flags : 'r',
115 | autoClose : true
116 | };
117 |
118 | var done=false;
119 | var list=[];
120 | this.createReadStream(null, uri, ps, (error, stream) => {
121 | if (error) {
122 | return callback(error);
123 | }
124 |
125 | stream.on('data', (buffer) => list.push(buffer));
126 |
127 | stream.on('end', () => {
128 | if (done) {
129 | return;
130 | }
131 | done=true;
132 | var body=Buffer.concat(list);
133 | if (encoding) {
134 | return callback(null, body.toString(encoding));
135 | }
136 | callback(null, body);
137 | });
138 |
139 | stream.on('error', (error) => {
140 | if (done) {
141 | return;
142 | }
143 | done=true;
144 |
145 | callback(error);
146 | });
147 | });
148 | }
149 |
150 | writeContent(uri, content, encoding, callback) {
151 | if (arguments.length==3) {
152 | callback=encoding;
153 | encoding=undefined;
154 | }
155 |
156 | var ps={
157 | flags : 'w',
158 | autoClose : true
159 | };
160 |
161 | this.createWriteStream(uri, ps, (error, stream) => {
162 | if (error) {
163 | return callback(error);
164 | }
165 |
166 | stream.end(content, encoding, (error) => {
167 | callback(error);
168 | });
169 | });
170 | }
171 |
172 | /**
173 | *
174 | */
175 | computeHash(uri, stats, callback) {
176 |
177 | this.createReadStream(null, uri, {
178 | flags : 'r',
179 | encoding : null,
180 | autoClose : true,
181 | start : 0,
182 | end : Math.min(stats.size, HASH_SIZE)
183 |
184 | }, (error, stream) => {
185 | var hash = crypto.createHash('sha256');
186 |
187 | hash.update(JSON.stringify({
188 | size : stats.size
189 | }));
190 |
191 | stream.on('data', (buffer) => hash.update(buffer));
192 |
193 | stream.on('end', () => {
194 | var digest = hash.digest("base64").replace(/=/g, '').replace(/\//g, '_');
195 |
196 | console.log("Hash of", uri, "=", digest);
197 |
198 | callback(null, digest);
199 | });
200 | });
201 | }
202 |
203 | /**
204 | *
205 | */
206 | newURL(url) {
207 | return new URL(this, url);
208 | }
209 |
210 | /**
211 | *
212 | */
213 | toString() {
214 | return "[ContentProvider name='"+this.name+"']";
215 | }
216 | }
217 |
218 |
219 | module.exports = ContentProvider;
220 |
--------------------------------------------------------------------------------
/lib/contentProviders/dropbox.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/lib/contentProviders/dropbox.js
--------------------------------------------------------------------------------
/lib/contentProviders/file.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, nomen: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const Mime = require('mime');
6 | const fs = require('fs');
7 | const Path = require('path');
8 | const Async = require('async');
9 | const debug = require('debug')('upnpserver:contentProviders:File');
10 | const crypto = require('crypto');
11 |
12 | const logger = require('../logger');
13 |
14 | const ContentProvider = require('./contentProvider');
15 |
16 | const DIRECTORY_MIME_TYPE = "inode/directory";
17 |
18 | const CHANGE_PATH_SEPARATOR = (Path.sep !== '/');
19 |
20 | const COMPUTE_HASH = false;
21 |
22 | class FileContentProvider extends ContentProvider {
23 |
24 | /**
25 | *
26 | */
27 | get isLocalFilesystem() {
28 | return true;
29 | }
30 |
31 | /**
32 | *
33 | */
34 | readdir(basePath, callback) {
35 |
36 | assert(typeof (basePath) === "string", "Base path is not a string (" +
37 | basePath + ")");
38 |
39 | var osPath = basePath;
40 | if (CHANGE_PATH_SEPARATOR) {
41 | osPath = osPath.replace(/\//g, Path.sep);
42 | }
43 |
44 | fs.readdir(osPath, (error, files) => {
45 | if (error) {
46 | return callback(error);
47 | }
48 |
49 | for (var i = 0; i < files.length; i++) {
50 | files[i] = this.newURL(basePath + '/' + files[i]);
51 | }
52 |
53 | debug("readdir", "returns basePath=", basePath, "=>", files);
54 |
55 | callback(null, files);
56 | });
57 | }
58 |
59 | /**
60 | *
61 | */
62 | stat(path, callback) {
63 | var osPath = path;
64 | if (CHANGE_PATH_SEPARATOR) {
65 | osPath = osPath.replace(/\//g, Path.sep);
66 | }
67 |
68 | fs.stat(osPath, (error, stats) => {
69 | if (error) {
70 | return callback(error);
71 | }
72 |
73 | var reg=/\/([^\/]+)$/.exec(path);
74 | if (reg) {
75 | stats.name=reg[1];
76 | }
77 |
78 | if (stats.isDirectory()) {
79 | stats.mimeType = DIRECTORY_MIME_TYPE;
80 |
81 | return callback(null, stats);
82 | }
83 |
84 | var mimeType = Mime.lookup(path, "");
85 | stats.mimeType = mimeType;
86 |
87 | if (!COMPUTE_HASH) {
88 | return callback(null, stats);
89 | }
90 |
91 | this.computeHash(path, stats, (error, hash) => {
92 |
93 | stats.sha256 = hash;
94 |
95 | callback(null, stats);
96 | });
97 | });
98 | }
99 |
100 | _mkdir(osPath, callback) {
101 | debug("_mkdir", "path=",osPath);
102 |
103 | fs.access(osPath, fs.R_OK | fs.W_OK, (error) => {
104 | if (error) {
105 | console.log("_mkdir", "parent=",osPath,"access problem=",error);
106 |
107 | if (error.code==='ENOENT') {
108 | var parent=Path.dirname(osPath);
109 |
110 | this._mkdir(parent, (error) => {
111 | if (error) {
112 | return callback(error);
113 | }
114 |
115 | fs.mkdir(osPath, callback);
116 | });
117 | return;
118 | }
119 |
120 | return callback(error);
121 | }
122 |
123 | callback();
124 | });
125 | }
126 |
127 | /**
128 | *
129 | */
130 | createWriteStream(url, options, callback) {
131 | debug("createWriteStream", "Url=",url,"options=",options);
132 |
133 | var osPath = url;
134 | if (CHANGE_PATH_SEPARATOR) {
135 | osPath = osPath.replace(/\//g, Path.sep);
136 | }
137 |
138 | var parent=Path.dirname(osPath);
139 |
140 | this._mkdir(parent, (error) => {
141 | if (error) {
142 | console.log("createWriteStream", "parent=",parent,"access problem=",error);
143 | return callback(error);
144 | }
145 |
146 | var stream = fs.createWriteStream(url, options);
147 |
148 | callback(null, stream);
149 | });
150 | }
151 |
152 | /**
153 | *
154 | */
155 | createReadStream(session, path, options, callback) {
156 | debug("createReadStream", "path=",path,"options=",options);
157 | assert(path, "Path parameter is null");
158 |
159 | var osPath = path;
160 | if (CHANGE_PATH_SEPARATOR) {
161 | osPath = osPath.replace(/\//g, Path.sep);
162 | }
163 |
164 | options = options || {}; // Fix for Nodejs v4
165 |
166 | var openStream = () => {
167 | if (debug.enabled) {
168 | debug("createReadStream", "osPath=", osPath, "options=", options);
169 | }
170 |
171 | try {
172 | var stream = fs.createReadStream(osPath, options);
173 |
174 | if (options && options.fd) {
175 | // Disable default destroy callback
176 | stream.destroy = () => {
177 | };
178 | }
179 |
180 | return callback(null, stream);
181 |
182 | } catch (x) {
183 | logger.error("Can not access to " + path, x);
184 |
185 | return callback(x);
186 | }
187 | };
188 |
189 | if (session) {
190 | options = options || {};
191 | options.flags = 'r';
192 | options.autoClose = false;
193 |
194 | if (debug.enabled) {
195 | debug("createReadStream", "Has session: path=", osPath, "session.fd=" + session.fd);
196 | }
197 |
198 | if (!session.fd) {
199 | fs.open(osPath, 'r', (error, fd) => {
200 | if (error) {
201 | return callback(error);
202 | }
203 | session.fd = fd;
204 | options.fd = fd;
205 |
206 | if (debug.enabled) {
207 | debug("createReadStream", "Has session open '" + osPath + "' => session.fd=" +
208 | session.fd);
209 | }
210 |
211 | openStream();
212 | });
213 |
214 | return;
215 | }
216 |
217 | options.fd = session.fd;
218 | }
219 |
220 | openStream();
221 | }
222 |
223 | /**
224 | *
225 | */
226 | end(session, callback) {
227 | if (session && session.fd) {
228 |
229 | if (debug.enabled) {
230 | debug("Close fd " + session.fd);
231 | }
232 |
233 | fs.close(session.fd, (error) => {
234 | delete session.fd;
235 |
236 | callback(error);
237 | });
238 | return;
239 | }
240 | callback();
241 | }
242 | }
243 |
244 | module.exports = FileContentProvider;
245 |
--------------------------------------------------------------------------------
/lib/contentProviders/googleDrive.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/lib/contentProviders/googleDrive.js
--------------------------------------------------------------------------------
/lib/contentProviders/http.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | var Util = require('util');
5 | var http = require('follow-redirects').http;
6 | var Url = require('url');
7 |
8 | var debug = require('debug')('upnpserver:contentProvider:Http');
9 | var logger = require('../logger');
10 |
11 | var ContentProvider = require('./contentProvider');
12 |
13 | var DIRECTORY_MIME_TYPE = "application/x-directory";
14 |
15 | class HttpContentProvider extends ContentProvider {
16 |
17 | /**
18 | *
19 | */
20 | readdir(url, callback) {
21 | callback(null, []);
22 | }
23 |
24 | /**
25 | *
26 | */
27 | stat(url, callback) {
28 | callback(null, {});
29 | }
30 |
31 | /**
32 | *
33 | */
34 | createReadStream(session, url, options, callback) {
35 |
36 | this._prepareRequestOptions(url, options, (error, requestOptions) => {
37 | if (error) {
38 | return callback(error);
39 | }
40 |
41 | var request = http.request(requestOptions);
42 |
43 | request.on('response', function(response) {
44 | if (Math.floor(response.statusCode / 100) !== 2) {
45 | return callback(new Error("Invalid status '" + response.statusCode +
46 | "' message='" + response.statusMessage + "' for url=" + url));
47 | }
48 |
49 | callback(null, response);
50 | });
51 |
52 | request.on('error', (error) => {
53 | logger("Error when loading url=",url,error);
54 | callback(error);
55 | });
56 | });
57 | }
58 |
59 | _prepareRequestOptions(url, options, callback) {
60 |
61 | var uoptions = Url.parse(url);
62 | uoptions.keepAlive = true;
63 |
64 | callback(null, uoptions);
65 | }
66 | }
67 |
68 | module.exports=HttpContentProvider;
69 |
--------------------------------------------------------------------------------
/lib/contentProviders/hubic.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oeuillot/upnpserver/8bf3cc5e7b174fc1bade7f332ce728fea3cb4611/lib/contentProviders/hubic.js
--------------------------------------------------------------------------------
/lib/db/abstractRegistry.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debug = require('debug')('upnpserver:db:abstractRegistry');
5 |
6 | class AbstractRegistry {
7 |
8 | /**
9 | *
10 | */
11 | keyFromString(key) {
12 | return key;
13 | }
14 |
15 | /**
16 | *
17 | */
18 | initialize(service, callback) {
19 | this._service = service;
20 | return callback(null);
21 | }
22 |
23 | /**
24 | *
25 | */
26 | registerNode(node, callback) {
27 | this.saveNode(node, null, callback);
28 | }
29 |
30 | getMetas(path, topic, callback) {
31 | callback(null);
32 | }
33 |
34 | putMetas(path, topic, metas, callback) {
35 | callback(null);
36 | }
37 | }
38 |
39 | module.exports = AbstractRegistry;
40 |
--------------------------------------------------------------------------------
/lib/db/cachedRegistry.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, nomen: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 |
6 | const NodeWeakHashmap = require('../util/nodeWeakHashmap');
7 | const debug = require('debug')('upnpserver:db:cachedRegistry');
8 |
9 | const AbstractRegistry = require('./abstractRegistry');
10 |
11 | const CACHE_DELAY_MS = 1000 * 10;
12 |
13 | class CachedRegistry extends AbstractRegistry {
14 |
15 | /**
16 | *
17 | */
18 | initialize(service, callback) {
19 |
20 | var garbage = (node, key, infos) => {
21 | debug("intialize.garbage", "Garbage node #", node.id, infos.readCount);
22 |
23 | var sem = node._isLocked();
24 | if (sem !== false) {
25 | debug("intialize.garbage", "Not releasable #", node.id, "locked by semaphore=", sem);
26 | this._map.put(node, node);
27 | return;
28 | }
29 |
30 | if (this._garbageNode) {
31 | this._garbageNode(node);
32 | }
33 | };
34 |
35 | this._map = new NodeWeakHashmap("nodeById", CACHE_DELAY_MS, false, garbage);
36 |
37 | debug("intialize", "CachedRegistry initialized");
38 |
39 | super.initialize(service, callback);
40 | }
41 |
42 | /**
43 | *
44 | */
45 | clear(callback) {
46 | assert(typeof(callback)==="function", "Invalid callback parameter");
47 |
48 | this._map.clear();
49 |
50 | debug("clear", "Clear all registry");
51 |
52 | callback(null);
53 | }
54 |
55 | /**
56 | *
57 | */
58 | saveNode(node, modifiedProperties, callback) {
59 | this._map.put(node, node);
60 |
61 | debug("saveNode", "Put in cache node #", node.id);
62 |
63 | callback(null, node);
64 | }
65 |
66 | /**
67 | *
68 | */
69 | getNodeById(id, callback) {
70 | assert(typeof(callback)==="function", "Invalid callback parameter");
71 |
72 | var node = this._map.get(id);
73 |
74 | debug("getNodeById", "Find node #", id, "=>", !!node);
75 |
76 | callback(null, node);
77 | }
78 |
79 | /**
80 | *
81 | */
82 | unregisterNode(node, callback) {
83 | assert(typeof(callback)==="function", "Invalid callback parameter");
84 |
85 | this._map.remove(node);
86 |
87 | debug("unregisterNode", "Unregister node #", node.id);
88 |
89 | callback(null, node);
90 | }
91 | }
92 |
93 | module.exports = CachedRegistry;
94 |
--------------------------------------------------------------------------------
/lib/db/memoryRegistry.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, nomen: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 |
6 | const AbstractRegistry = require('./abstractRegistry');
7 | const Node = require('../node');
8 |
9 | //IT MUST START AT 0 because UPNP ROOT must have id 0
10 | var nodeIndex = 10;
11 |
12 | class MemoryRegistry extends AbstractRegistry {
13 |
14 | /**
15 | *
16 | */
17 | initialize(service, callback) {
18 | this._dbMap = {};
19 | this._count = 0;
20 | this._repositoryById = {};
21 | this._repositoryCount = 0;
22 |
23 | super.initialize(service, callback);
24 | }
25 |
26 | /**
27 | *
28 | */
29 | keyFromString(key) {
30 | return parseInt(key, 10);
31 | }
32 |
33 | /**
34 | *
35 | */
36 | clear(callback) {
37 | this._dbMap = {};
38 | this._count = 0;
39 |
40 | callback(null);
41 | }
42 |
43 | /**
44 | *
45 | */
46 | saveNode(node, modifiedProperties, callback) {
47 | this._dbMap[node.id] = node;
48 |
49 | callback(null, node);
50 | }
51 |
52 | /**
53 | *
54 | */
55 | getNodeById(id, callback) {
56 | var node = this._dbMap[id];
57 |
58 | setImmediate(() => {
59 | callback(null, node);
60 | });
61 | }
62 |
63 | /**
64 | *
65 | * @param {Node} node
66 | * @param {Function} callback
67 | */
68 | unregisterNode(node, callback) {
69 | assert(node instanceof Node, "Invalid node parameter");
70 | assert(typeof(callback) === "function", "Invalid callback parameter");
71 |
72 | var id = node.id;
73 | if (!this._dbMap[id]) {
74 | return callback("Node not found");
75 | }
76 |
77 | delete this._dbMap[id];
78 | this._count--;
79 |
80 | callback();
81 | }
82 |
83 | /**
84 | *
85 | */
86 | allocateNodeId(node, callback) {
87 | node._id = nodeIndex++;
88 | this._count++;
89 |
90 | callback();
91 | }
92 |
93 | registerRepository(repository, repositoryHashKey, callback) {
94 | repository._id = this._repositoryCount++;
95 | this._repositoryById[repository._id] = repository;
96 |
97 | callback(null, repository);
98 | }
99 | }
100 |
101 | module.exports = MemoryRegistry;
102 |
--------------------------------------------------------------------------------
/lib/db/mongodbRegistry.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus: true, nomen: true, vars: true, esversion: 6 */
2 | "use strict";
3 |
4 | const MongoDb = require('mongodb');
5 | const Async = require('async');
6 | const crypto = require('crypto');
7 |
8 | const debug = require('debug')('upnpserver:db:mongodb');
9 | const logger = require('../logger');
10 |
11 | const NeDbRegistry = require('./nedbRegistry');
12 |
13 | class MongoDbRegistry extends NeDbRegistry {
14 |
15 | /**
16 | *
17 | */
18 | initializeDb(callback) {
19 | var url = process.env.MONGODB_URL; //'mongodb://localhost:27017/upnpserver';
20 |
21 | if (!url) {
22 | var error=new Error("You must specify MONGODB_URL environment variable");
23 | return callback(error);
24 | }
25 |
26 | debug("Connect client to url",url);
27 |
28 | MongoDb.MongoClient.connect(url, (error, db) => {
29 | if (error) {
30 | logger.error("Can not connect mongodb server", url, error);
31 | return callback(error);
32 | }
33 |
34 | debug("Mongodb connected");
35 |
36 | var collection = db.collection('nodes');
37 | this._configureNodesDb(collection, (error) => {
38 | debug("NodesDb",collection,error);
39 |
40 | if (error) {
41 | return callback(error);
42 | }
43 | this._nodesCollection = collection;
44 |
45 | var collection2 = db.collection('metas');
46 | this._configureMetasDb(collection2, (error) => {
47 | debug("MetasDb",collection2,error);
48 | if (error) {
49 | return callback(error);
50 | }
51 |
52 | this._metasCollection = collection2;
53 |
54 | var collection3 = db.collection('repositories');
55 | this._configureRepositoriesDb(collection3, (error) => {
56 | debug("RepositoriesDb",collection3,error);
57 | if (error) {
58 | return callback(error);
59 | }
60 |
61 | this._repositoriesCollection = collection3;
62 |
63 | callback();
64 | });
65 | });
66 | });
67 | });
68 | }
69 |
70 | /**
71 | *
72 | */
73 | _ensureIndexes(collection, fields, callback) {
74 | Async.eachSeries(fields, (f, callback) => {
75 |
76 | debug("Ensure Index",f);
77 |
78 | collection.ensureIndex(f.fieldName, {
79 | unique: f.unique,
80 | sparse: f.sparse
81 |
82 | }, (error) => {
83 | debug("Index done",error);
84 |
85 | callback(error);
86 | });
87 |
88 | }, (error) => {
89 | if (error) {
90 | logger.error(error);
91 | return callback(null, error);
92 | }
93 |
94 | debug("Indexes installed !");
95 |
96 | callback();
97 | });
98 | }
99 |
100 | allocateNodeId(node, callback) {
101 | var objectID = new MongoDb.ObjectID();
102 |
103 | node.$id=objectID;
104 | node._id=this._convertObjectIDToId(objectID);
105 |
106 | debug("Allocated id=", objectID);
107 |
108 | callback();
109 | }
110 |
111 | /**
112 | *
113 | */
114 | _convertObjectIDToId(id, cache) {
115 | if (this.$rootId && this.$rootId.equals(id)) {
116 | return 0;
117 | }
118 | return String(id);
119 | }
120 |
121 | /**
122 | *
123 | */
124 | _convertIdToObjectID(id, cache) {
125 | if (id===0) {
126 | return this.$rootId;
127 | }
128 | return new MongoDb.ObjectID(id);
129 | }
130 |
131 |
132 | /**
133 | *
134 | */
135 | _fillChildrenAndLinkIds(node, objectID, callback) {
136 |
137 | this._nodesCollection.find( { parentId: objectID }).project({ _id: 1 }).toArray((error, docs) => {
138 | debug("Find children by parentId #", objectID, "=>", docs, "error=", error);
139 | if (error) {
140 | logger.error(error);
141 | return callback(error);
142 | }
143 |
144 | if (docs.length) {
145 | node.childrenIds=docs.map((doc) => this._convertObjectIDToId(doc._id));
146 | }
147 | debug("Node.childrenIds #", objectID, "=>", node.childrenIds);
148 |
149 | this._nodesCollection.find( { refId: objectID }).project({ _id: 1}).toArray((error, docs) => {
150 | debug("Find linked by node #", objectID, "=>", docs, "error=", error);
151 | if (error) {
152 | logger.error(error);
153 | return callback(error);
154 | }
155 |
156 | if (docs.length) {
157 | node.linkedIds=docs.map((doc) => this._convertObjectIDToId(doc._id));
158 | }
159 | debug("Node.linkedIds #", objectID, "=>", node.linkedIds);
160 |
161 | this._saveNode(node, null, callback);
162 | });
163 | });
164 | }
165 |
166 | }
167 |
168 | module.exports = MongoDbRegistry;
169 |
--------------------------------------------------------------------------------
/lib/db/mysqlRegistry.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus: true, nomen: true, vars: true */
2 | "use strict";
3 |
4 | var mysql = require('mysql');
5 |
6 | var MysqlRegistry = function() {
7 |
8 | };
9 |
10 | module.exports = MysqlRegistry;
11 |
12 | MysqlRegistry.prototype.initialize = function(service, callback) {
13 | var connection = mysql.createConnection({
14 | host : 'example.org',
15 | user : 'bob',
16 | password : 'secret'
17 | });
18 |
19 | };
20 |
21 | MysqlRegistry.prototype.registerNode = function(item, callback) {
22 |
23 | return callback(null, item);
24 | };
25 |
26 | MysqlRegistry.prototype.getNodeById = function(id, callback) {
27 |
28 | return callback(null, null);
29 |
30 | };
31 |
--------------------------------------------------------------------------------
/lib/filterSearchEngine.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, sub: true, esversion: 6 */
2 | "use strict";
3 |
4 | var debug = require('debug')('upnpserver:filterSearchEngine');
5 |
6 | var Xmlns = require('./xmlns');
7 |
8 | var _splitXmlnsNameRegExp = /([^:]+:)?([^@]+)(@.*)?$/i;
9 |
10 | function returnTRUE() {
11 | return true;
12 | }
13 |
14 | var defaultNamespaceURIs = {
15 | "" : Xmlns.DIDL_LITE,
16 | dc : Xmlns.PURL_ELEMENT,
17 | upnp : Xmlns.UPNP_METADATA,
18 |
19 | // Lame FREEBOX xmlns declaration !!!!
20 | sec : Xmlns.SEC_DLNA
21 | };
22 |
23 | var defaultFilters = {
24 | [Xmlns.DIDL_LITE] : {
25 | "item" : {
26 | id : true,
27 | parentID : true,
28 | refID : true,
29 | restricted : true
30 | },
31 | "container" : {
32 | id : true,
33 | parentID : true,
34 | refID : true,
35 | restricted : true,
36 | childCount : true
37 | },
38 | "res": {
39 | protocolInfo: true
40 | }
41 | },
42 | [Xmlns.UPNP_METADATA] : {
43 | "class" : {
44 | '*' : true
45 | }
46 | },
47 | [Xmlns.PURL_ELEMENT] : {
48 | "title" : {
49 | '*' : true
50 | }
51 | }
52 | };
53 |
54 | module.exports = class FilterSearchEngine {
55 | /**
56 | *
57 | */
58 | constructor(contentDirectoryService, filterNode, searchNode) {
59 | this.contentDirectoryService = contentDirectoryService;
60 | this.filterNode = filterNode;
61 | this.searchNode = searchNode;
62 |
63 | if (filterNode) {
64 | var func = prepareFilterCallback(filterNode.val, filterNode.namespaceURIs);
65 |
66 | if (func) {
67 | this._filterFunc = func;
68 |
69 | } else {
70 | filterNode = null;
71 | }
72 | }
73 |
74 | if (!this._filterFunc && !this._searchFunc) {
75 | // No functions at all
76 |
77 | this.func = returnTRUE;
78 | return;
79 | }
80 |
81 | this.func=this.process.bind(this);
82 | }
83 |
84 | /**
85 | *
86 | */
87 | start(node) {
88 | this.currentNode = node;
89 | this._ignore=undefined;
90 | }
91 |
92 | /**
93 | *
94 | */
95 | process(ns, element, attribute) {
96 | if (this._ignore) {
97 | return false;
98 | }
99 |
100 | var filterFunc = this._filterFunc;
101 | if (filterFunc) {
102 | return filterFunc(ns, element, attribute);
103 | }
104 |
105 | return true;
106 | }
107 |
108 | /**
109 | *
110 | */
111 | end(jxml) {
112 | this.currentNode = null;
113 |
114 | if (this._ignore) {
115 | return null;
116 | }
117 | return jxml;
118 | }
119 | };
120 |
121 | function prepareFilterCallback(filterExpression, namespaceURIs) {
122 | if (!filterExpression || filterExpression === "*") {
123 | return false;
124 | }
125 |
126 | var filters = {};
127 |
128 | filterExpression.split(',').forEach((token) => {
129 | var sp = _splitXmlnsNameRegExp.exec(token);
130 | if (!sp) {
131 | console.error("Unknown filter token format '" + token + "'");
132 | return;
133 | }
134 |
135 | // console.log("Register: ", sp);
136 |
137 | var prefix = (sp[1] && sp[1].slice(0, -1)) || "";
138 | var element = sp[2];
139 | var attribute = (sp[3] && sp[3].slice(1)) || "*";
140 |
141 | var xmlns = namespaceURIs[prefix] || defaultNamespaceURIs[prefix];
142 | if (!xmlns) {
143 | debug("Unknown xmlns for prefix", prefix, " token=", token,
144 | " namespaceURIs=", namespaceURIs);
145 | return;
146 | }
147 |
148 | var fs = filters[xmlns];
149 | if (!fs) {
150 | fs = {};
151 | filters[xmlns] = fs;
152 | }
153 |
154 | var elt = fs[element];
155 | if (!elt) {
156 | elt = {};
157 | fs[element] = elt;
158 | }
159 |
160 | elt[attribute] = true;
161 | });
162 |
163 | return (ns, element, attribute) => {
164 | if (!attribute) {
165 | attribute = "*";
166 | }
167 |
168 | var df = defaultFilters[ns];
169 | var dfe;
170 | if (df) {
171 | dfe = df[element];
172 | if (dfe && (dfe[attribute] || dfe['*'])) {
173 | return true;
174 | }
175 | }
176 |
177 | df = filters[ns];
178 | if (df) {
179 | dfe = df[element];
180 | if (dfe && (dfe[attribute] || dfe['*'])) {
181 | return true;
182 | }
183 | }
184 |
185 | return false;
186 | };
187 | }
188 |
--------------------------------------------------------------------------------
/lib/i18n/de.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | module.exports = {
5 | ARTISTS_FOLDER : "Künstler",
6 | ALBUMS_FOLDER : "Alben",
7 |
8 | RECENTS : "Kürzlich",
9 | ALL_DEVICES : "Alle Geräte",
10 | TRACKS_FOLDER : "Titel",
11 | IMAGES_FOLDER : "Bilder",
12 | VIDEOS_FOLDER : "Videos",
13 |
14 | BY_ACTORS_FOLDER : "Nach Autor",
15 | BY_YEARS_FOLDER : "Nach Jahr",
16 | BY_TITLE_FOLDER : "Nach Titel",
17 | BY_ORIGINAL_TITLE_FOLDER : "Nach Originaltitel",
18 | BY_KEYWORDS_FOLDER : "Nach Schlüsselwörtern",
19 | _3D_MOVIES_FOLDER: "3D Filme",
20 |
21 | UNKNOWN_ARTIST : "Unbekannter Künstler",
22 | UNKNOWN_ALBUM : "Unbekanntes Album",
23 | UNKNOWN_GENRE : "Unbekanntes Genre",
24 | UNKNOWN_TITLE : "Unbekannter Titel",
25 |
26 | ROOT_NAME : "Wurzelverzeichnis"
27 | };
28 |
--------------------------------------------------------------------------------
/lib/i18n/en.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | module.exports = {
5 | ARTISTS_FOLDER : "Artists",
6 | ALBUMS_FOLDER : "Albums",
7 |
8 | RECENTS : "Recents",
9 | ALL_DEVICES : "All devices",
10 | TRACKS_FOLDER : "Tracks",
11 | IMAGES_FOLDER : "Images",
12 | VIDEOS_FOLDER : "Videos",
13 |
14 | BY_ACTORS_FOLDER : "By actors",
15 | BY_YEARS_FOLDER : "By years",
16 | BY_TITLE_FOLDER : "By title",
17 | BY_ORIGINAL_TITLE_FOLDER : "By original title",
18 | BY_KEYWORDS_FOLDER : "By keywords",
19 | _3D_MOVIES_FOLDER: "3D Movies",
20 |
21 | UNKNOWN_ARTIST : "Unknown artist",
22 | UNKNOWN_ALBUM : "Unknown album",
23 | UNKNOWN_GENRE : "Unknown genre",
24 | UNKNOWN_TITLE : "Unknown title",
25 |
26 | ROOT_NAME : "Root"
27 | };
--------------------------------------------------------------------------------
/lib/i18n/fr.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | module.exports = {
5 | ARTISTS_FOLDER : "Artistes",
6 | ALBUMS_FOLDER : "Albums",
7 |
8 | RECENTS : "Récents",
9 | ALL_DEVICES : "Tous les appareils",
10 | TRACKS_FOLDER : "Pistes audio",
11 | IMAGES_FOLDER : "Images",
12 | VIDEOS_FOLDER : "Vidéos",
13 |
14 | BY_ACTORS_FOLDER : "Par acteurs",
15 | BY_YEARS_FOLDER : "Par années",
16 | BY_TITLE_FOLDER : "Par nom",
17 | BY_ORIGINAL_TITLE_FOLDER : "Par nom original",
18 | BY_KEYWORDS_FOLDER : "Par sujets",
19 | _3D_MOVIES_FOLDER: "Films 3D",
20 |
21 |
22 | UNKNOWN_ARTIST : "Artiste inconnu",
23 | UNKNOWN_ALBUM : "Album inconnu",
24 | UNKNOWN_GENRE : "Genre inconnu",
25 | UNKNOWN_TITLE : "Titre inconnu",
26 |
27 | ROOT_NAME : "Racine"
28 | };
29 |
--------------------------------------------------------------------------------
/lib/i18n/kr.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | module.exports = {
5 | ARTISTS_FOLDER : "아티스트",
6 | ALBUMS_FOLDER : "앨범",
7 |
8 | RECENTS : "최근",
9 | ALL_DEVICES : "모든 장치",
10 | TRACKS_FOLDER : "음악",
11 | IMAGES_FOLDER : "사진",
12 | VIDEOS_FOLDER : "비디오",
13 |
14 | BY_ACTORS_FOLDER : "아티스트별",
15 | BY_YEARS_FOLDER : "연도별",
16 | BY_TITLE_FOLDER : "제목별",
17 | BY_ORIGINAL_TITLE_FOLDER : "원제목별",
18 | BY_KEYWORDS_FOLDER : "키워드별",
19 | _3D_MOVIES_FOLDER: "3D 영화",
20 |
21 | UNKNOWN_ARTIST : "알 수 없는 아티스트",
22 | UNKNOWN_ALBUM : "알 수 없는 앨범",
23 | UNKNOWN_GENRE : "알 수 없는 장르",
24 | UNKNOWN_TITLE : "알 수 없는 제목",
25 |
26 | ROOT_NAME : "루트"
27 | };
28 |
--------------------------------------------------------------------------------
/lib/i18n/lt.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | module.exports = {
5 | ARTISTS_FOLDER : "Atlikėjai",
6 |
7 | ALBUMS_FOLDER : "Albums?", // TODO
8 |
9 | RECENTS : "Recents?", // TODO
10 | ALL_DEVICES : "All devices?", // TODO
11 | TRACKS_FOLDER : "Tracks?", // TODO
12 | IMAGES_FOLDER : "Images?", // TODO
13 | VIDEOS_FOLDER : "Videos?", // TODO
14 |
15 | BY_ACTORS_FOLDER : "By actors?", // TODO
16 | BY_YEARS_FOLDER : "By years?", // TODO
17 | BY_TITLE_FOLDER : "By title?", // TODO
18 | BY_ORIGINAL_TITLE_FOLDER : "By original title?", // TODO
19 | BY_KEYWORDS_FOLDER : "By keywords?", // TODO
20 | _3D_MOVIES_FOLDER: "3D Movies?", // TODO
21 |
22 | UNKNOWN_ARTIST : "Nežinomas atlikėjas",
23 | UNKNOWN_ALBUM : "Nežinomas albumas",
24 | UNKNOWN_GENRE : "Nežinomas žanras",
25 | UNKNOWN_TITLE : "Nežinomas pavadinimas",
26 |
27 | ROOT_NAME : "Tėvinis"
28 | };
29 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, debug: true */
2 | "use strict";
3 |
4 | var debug = require('debug')('upnpserver');
5 |
6 | var Logger = {
7 |
8 | log : function() {
9 | debugger;
10 | throw 'Do not use Logger.log function';
11 | },
12 |
13 | trace : console.log.bind(console),
14 | debug : console.log.bind(console),
15 | verbose : console.log.bind(console),
16 | info : console.info.bind(console) || console.log.bind(console),
17 | warn : console.warn.bind(console) || console.log.bind(console),
18 | error : console.error.bind(console) || console.log.bind(console)
19 | };
20 |
21 | if (debug.enabled) {
22 | Logger.debug = debug;
23 | Logger.verbose = debug;
24 | Logger.info = debug;
25 | Logger.warn = debug;
26 | Logger.error = debug;
27 | }
28 |
29 | module.exports = Logger;
--------------------------------------------------------------------------------
/lib/mediaReceiverRegistrarService.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, sub: true, esversion: 6 */
2 | "use strict";
3 |
4 | var Util = require('util');
5 | var debug = require('debug')('upnpserver:mediaReceiverRegistrarService');
6 |
7 | var Service = require("./service");
8 | var Xmlns = require('./xmlns');
9 |
10 | class MediaReceiverRegistrar extends Service {
11 | constructor() {
12 | super({
13 | serviceType : "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
14 | serviceId : "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar",
15 | route : "mrr"
16 | });
17 |
18 | this.addAction("IsAuthorized", [ {
19 | name : "DeviceID",
20 | type : "A_ARG_TYPE_DeviceID"
21 | } ], [ {
22 | name : "Result",
23 | type : "A_ARG_TYPE_Result"
24 | } ]);
25 | this.addAction("IsValidated", [ {
26 | name : "DeviceID",
27 | type : "A_ARG_TYPE_DeviceID"
28 | } ], [ {
29 | name : "Result",
30 | type : "A_ARG_TYPE_Result"
31 | } ]);
32 | this.addAction("RegisterDevice", [ {
33 | name : "RegistrationReqMsg",
34 | type : "A_ARG_TYPE_RegistrationReqMsg"
35 | } ], [ {
36 | name : "RegistrationRespMsg",
37 | type : "A_ARG_TYPE_RegistrationRespMsg"
38 | } ]);
39 |
40 | this.addType("A_ARG_TYPE_DeviceID", "string");
41 | this.addType("A_ARG_TYPE_RegistrationReqMsg", "bin.base64");
42 | this.addType("A_ARG_TYPE_RegistrationRespMsg", "bin.base64");
43 | this.addType("A_ARG_TYPE_Result", "int", 1);
44 | this.addType("AuthorizationDeniedUpdateID", "ui4", 1, [], {
45 | dt : Xmlns.MICROSOFT_DATATYPES
46 | }, true);
47 | this.addType("AuthorizationGrantedUpdateID", "ui4", 1, [], {
48 | dt : Xmlns.MICROSOFT_DATATYPES
49 | }, true);
50 | this.addType("ValidationRevokedUpdateID", "ui4", 1, [], {
51 | dt : Xmlns.MICROSOFT_DATATYPES
52 | }, true);
53 | this.addType("ValidationSucceededUpdateID", "ui4", 1, [], {
54 | dt : Xmlns.MICROSOFT_DATATYPES
55 | }, true);
56 | }
57 |
58 | /**
59 | *
60 | */
61 | processSoap_RegisterDevice(xml, request, response, callback) {
62 |
63 | return callback();
64 | }
65 |
66 | /**
67 | *
68 | */
69 | processSoap_IsAuthorized(xml, request, response, callback) {
70 |
71 | var deviceID = Service._childNamed(xml, "DeviceID");
72 |
73 | debug("IsAuthorized('" + deviceID + "')");
74 |
75 | this.responseSoap(response, "IsAuthorized", {
76 | _name : "u:IsAuthorizedResponse",
77 | _attrs : {
78 | "xmlns:u" : this.type
79 | },
80 | _content : {
81 | Result : {
82 | _attrs : {
83 | "xmlns:dt" : Xmlns.MICROSOFT_DATATYPES,
84 | "dt:dt" : "int"
85 | },
86 | _content : this.stateVars["A_ARG_TYPE_Result"].get()
87 | }
88 | }
89 | }, callback);
90 | }
91 |
92 | /**
93 | *
94 | */
95 | processSoap_IsValidated(xml, request, response, callback) {
96 |
97 | var deviceID = Service._childNamed(xml, "DeviceID");
98 |
99 | debug("IsValidated(" , deviceID , ")");
100 |
101 | this.responseSoap(response, "IsValidated", {
102 | _name : "u:IsValidatedResponse",
103 | _attrs : {
104 | "xmlns:u" : this.type
105 | },
106 | _content : {
107 | Result : {
108 | _attrs : {
109 | "xmlns:dt" : Xmlns.MICROSOFT_DATATYPES,
110 | "dt:dt" : "int"
111 | },
112 | _content : this.stateVars["A_ARG_TYPE_Result"].get()
113 | }
114 | }
115 | }, callback);
116 | }
117 | }
118 |
119 |
120 | module.exports = MediaReceiverRegistrar;
121 |
--------------------------------------------------------------------------------
/lib/repositories/history.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const Async = require('async');
5 | const Util = require('util');
6 | const debug = require('debug')('upnpserver:repositories:History');
7 |
8 | const logger = require('../logger');
9 | const Repository = require('./repository');
10 |
11 | var TYPE_BY_CLASS = {
12 | "object.item.videoItem" : "videos",
13 | "object.item.audioItem" : "tracks",
14 | "object.item.imageItem" : "images",
15 | "object.container.album.musicAlbum" : "albums"
16 | };
17 |
18 | class HistoryRepository extends Repository {
19 | constructor(mountPath, configuration) {
20 | super(mountPath, configuration);
21 |
22 | var perHostHistorySize=this.configuration.perHostHistorySize;
23 | var allHostHistorySize=this.configuration.allHostHistorySize;
24 |
25 | if (!perHostHistorySize || perHostHistorySize < 0) {
26 | perHostHistorySize = 4;
27 | }
28 | if (!allHostHistorySize || allHostHistorySize < 0) {
29 | allHostHistorySize = 4;
30 | }
31 |
32 | this.perHostHistorySize = perHostHistorySize;
33 | this.allHostHistorySize = allHostHistorySize;
34 |
35 | this._folders = {};
36 | }
37 |
38 | get type() {
39 | return "history";
40 | }
41 | /**
42 | *
43 | */
44 | initialize(service, callback) {
45 |
46 | service.on("BrowseDirectChildren", (request, node) => {
47 |
48 | var nodeType = this._getNodeType(node);
49 |
50 | if (debug.enabled) {
51 | debug("BrowseDirectChildren node=#", node.id, " => nodeType=", nodeType);
52 | }
53 | if (!nodeType) {
54 | return;
55 | }
56 |
57 | var clientId = this._getClientId(request);
58 | if (debug.enabled) {
59 | debug("Request clientId=", clientId, " for request ", request);
60 | }
61 | if (!clientId) {
62 | return;
63 | }
64 |
65 | setTimeout(() => {
66 | this._registerNewRef(nodeType, node, clientId, (error) => {
67 | if (error) {
68 | console.error(error);
69 | return;
70 | }
71 |
72 | });
73 | }, 100);
74 | });
75 |
76 | service.on("request", (request, nodeRef, node, parameters) => {
77 |
78 | if (parameters.contentHandler) {
79 | return;
80 | }
81 |
82 | var nodeType = this._getNodeType(nodeRef);
83 |
84 | if (debug.enabled) {
85 | debug("Request ref=#", nodeRef.id, " node=#", node.id, " => nodeType=",
86 | nodeType);
87 | }
88 | if (!nodeType) {
89 | return;
90 | }
91 |
92 | var clientId = this._getClientId(request);
93 | if (debug.enabled) {
94 | debug("Request clientId=", clientId, " for request ", request);
95 | }
96 | if (!clientId) {
97 | return;
98 | }
99 |
100 | setTimeout(() => {
101 | this._registerNewRef(nodeType, node, clientId, (error) => {
102 | if (error) {
103 | console.error(error);
104 | return;
105 | }
106 |
107 | });
108 | }, 100);
109 | });
110 |
111 | service.on("filterList", (request, node, list) => {
112 |
113 | if (node.id !== this._mountNode.id) {
114 | return;
115 | }
116 |
117 | var clientId = this._getClientId(request);
118 | if (debug.enabled) {
119 | debug("Filter list of mount node #", this._mountNode.id, " clientId=",
120 | clientId);
121 | }
122 |
123 | if (!clientId) {
124 | return;
125 | }
126 |
127 | for (var i = 0; i < list.length;) {
128 | var n = list[i];
129 | var nClientId = n.attributes && n.attributes.clientId;
130 | if (!nClientId) {
131 | i++;
132 | continue;
133 | }
134 |
135 | if (nClientId === clientId) {
136 | i++;
137 | continue;
138 | }
139 |
140 | if (debug.enabled) {
141 | debug("Remove clientId", nClientId, "from list");
142 | }
143 |
144 | list.splice(i, 1);
145 | }
146 | });
147 |
148 | var i18n = service.upnpServer.configuration.i18n;
149 |
150 | super.initialize(service, (error, node) => {
151 | if (error) {
152 | return callback(error);
153 | }
154 |
155 | this._mountNode = node;
156 |
157 | this.newVirtualContainer(node, i18n.ALL_DEVICES, (error, allNode) => {
158 | if (error) {
159 | return callback(error);
160 | }
161 |
162 | this._allNode = allNode;
163 |
164 | this._declareFolders(allNode, "*", (error) => {
165 | if (error) {
166 | return callback(error);
167 | }
168 |
169 | callback(null, node);
170 | });
171 | });
172 | });
173 | }
174 |
175 | /**
176 | *
177 | */
178 | _declareFolders(parentNode, clientId, callback) {
179 |
180 | var fs = this._folders[clientId];
181 | if (fs) {
182 | return callback(null, fs);
183 | }
184 |
185 | fs = {};
186 | this._folders[clientId] = fs;
187 |
188 | debug("Create folders on #" + parentNode.id);
189 |
190 | var i18map = {
191 | tracks : "TRACKS_FOLDER",
192 | albums : "ALBUMS_FOLDER",
193 | videos : "VIDEOS_FOLDER",
194 | images : "IMAGES_FOLDER"
195 | };
196 |
197 | var i18n = this.service.upnpServer.configuration.i18n;
198 |
199 | Async.eachSeries([ 'tracks', 'albums', 'videos', 'images' ], (type, callback) => {
200 |
201 | var label = i18n[i18map[type]];
202 |
203 | this.newVirtualContainer(parentNode, label, (error, node) => {
204 | if (error) {
205 | debug("_declareFolders: newVirtualContainer parentNode=",
206 | parentNode.id, " label=", label, " error=", error);
207 | return callback(error);
208 | }
209 |
210 | fs[type] = node;
211 | node.attributes.clientId = clientId;
212 | node.attributes.defaultSort = "-dc:date";
213 |
214 | callback(null, node);
215 | });
216 | }, callback);
217 | }
218 |
219 | _getClientId(request) {
220 | var headers = request.headers;
221 | var xForwardedFor = headers["x-forwarded-for"] ||
222 | headers["x-cluster-client-ip"] || headers["x-real-ip"];
223 | if (xForwardedFor) {
224 | return xForwardedFor;
225 | }
226 |
227 | var remoteAddress = request.connection.remoteAddress ||
228 | request.socket.remoteAddress || request.connection.socket.remoteAddress;
229 |
230 | return remoteAddress || "Inconnu"; // TODO Unknown
231 | }
232 |
233 | _getNodeType(node) {
234 | var clazz = node.upnpClass;
235 | if (!clazz) {
236 | debug("Class=NULL ? #", node.id);
237 | return null;
238 | }
239 |
240 | debug("Class=", clazz, clazz && clazz.name);
241 |
242 | var cname = clazz.name;
243 |
244 | for ( var k in TYPE_BY_CLASS) {
245 | if (cname.indexOf(k)) {
246 | continue;
247 | }
248 |
249 | debug("Found ", cname, " => ", k, " ", TYPE_BY_CLASS[k]);
250 |
251 | return TYPE_BY_CLASS[k];
252 | }
253 |
254 | debug("Not found", cname, "?");
255 |
256 | return null;
257 | }
258 |
259 | _registerNewRef(nodeType, node,
260 | clientId, callback) {
261 |
262 | this._declareFolders(this._mountNode, clientId, (error, folderNode) => {
263 | if (error) {
264 | return callback(error);
265 | }
266 |
267 | var parent = this._folders[clientId][nodeType];
268 | if (!parent) {
269 | return callback();
270 | }
271 |
272 | this._removeNodeRef(parent, node, this.perHostHistorySize, (error) => {
273 | if (error) {
274 | return callback(error);
275 | }
276 |
277 | this.newNodeRef(parent, node, null, (newNode) => {
278 |
279 | newNode.attributes = newNode.attributes || {};
280 | newNode.attributes.date = Date.now();
281 |
282 | }, (error, newNode) => {
283 | if (error) {
284 | return callback(error);
285 | }
286 |
287 | this._declareFolders(this._allNode, "*", (error, folderNode) => {
288 | if (error) {
289 | return callback(error);
290 | }
291 |
292 | var parent = this._folders["*"][nodeType];
293 | if (!parent) {
294 | return callback();
295 | }
296 |
297 | this._removeNodeRef(parent, node, this.allHostHistorySize, (error) => {
298 | if (error) {
299 | return callback(error);
300 | }
301 |
302 | this.newNodeRef(parent, node, null, (newNode) => {
303 |
304 | newNode.attributes = newNode.attributes || {};
305 | newNode.attributes.date = Date.now();
306 |
307 | }, callback);
308 | });
309 | });
310 | });
311 | });
312 | });
313 | }
314 |
315 | _removeNodeRef(parent, nodeRef,
316 | maxListSize, callback) {
317 |
318 | parent.listChildren((error, list) => {
319 | if (error) {
320 | return callback(error);
321 | }
322 |
323 | // console.log("List of #", parent.id, "=>", list);
324 |
325 | var cnt = 0;
326 |
327 | Async.eachSeries(list, (node, callback) => {
328 | cnt++;
329 |
330 | // debug("Test node=", node);
331 | if (!node) {
332 | debug("WARNING !!!! node is null ???");
333 | return callback();
334 | }
335 |
336 | if (node.refId !== nodeRef.id && (!maxListSize || cnt < maxListSize)) {
337 | return callback();
338 | }
339 |
340 | // debug("Remove already referenced node ! #" + node.id);
341 |
342 | parent.removeChild(node, callback);
343 | }, callback);
344 | });
345 | }
346 | }
347 |
348 | module.exports = HistoryRepository;
349 |
--------------------------------------------------------------------------------
/lib/repositories/path.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus:true, node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const Path = require('path');
6 | const Async = require('async');
7 |
8 | const debug = require('debug')('upnpserver:repositories:Path');
9 | const logger = require('../logger');
10 |
11 | const Node = require('../node');
12 | const UpnpContainer = require('../class/object.container');
13 | const Repository = require('./repository');
14 | const URL = require('../util/url');
15 |
16 | const SAVE_ACCESSTIME = false;
17 |
18 | /**
19 | *
20 | */
21 | class PathRepository extends Repository {
22 | constructor(mountPath, configuration) {
23 | super(mountPath, configuration);
24 |
25 | var path=this.configuration.path;
26 |
27 | assert.equal(typeof (path), "string", "Invalid path parameter");
28 |
29 | debug("PathRepository", "constructor path=",path);
30 |
31 | if (Path.sep !== '/') {
32 | path = path.replace(/\\/g, '/');
33 | }
34 | this._directoryPath = path;
35 | }
36 |
37 | /**
38 | *
39 | */
40 | get hashKey() {
41 | return {
42 | type: this.type,
43 | mountPath: this.mountPath,
44 | directoryPath: this._directoryPath
45 | };
46 | }
47 |
48 | /**
49 | *
50 | */
51 | initialize(service, callback) {
52 | var path=this._directoryPath;
53 |
54 | this._directoryURL = service.newURL(path);
55 |
56 | super.initialize(service, callback);
57 | }
58 |
59 | get directoryURL() {
60 | return this._directoryURL;
61 | }
62 |
63 | /**
64 | *
65 | */
66 | static FillAttributes(node, stats) {
67 | var attributes=node.attributes;
68 |
69 | if (attributes.size===undefined &&!node.service.upnpServer.configuration.strict) {
70 | attributes.size = stats.size;
71 | }
72 |
73 | if (stats.mimeType) {
74 | attributes.mimeType = stats.mimeType;
75 | }
76 |
77 | /* node.contentTime is the modifiedTime
78 | var mtime = stats.mtime;
79 | if (mtime) {
80 | if (mtime.getFullYear() >= 1970) {
81 | attributes.modifiedTime = mtime.getTime();
82 | } else {
83 | attributes.modifiedTime = mtime;
84 | }
85 | }
86 | */
87 | var mtime=node.currentTime;
88 |
89 | var ctime = stats.ctime;
90 | if (ctime) {
91 | if (ctime.getFullYear() >= 1970) {
92 | attributes.changeTime = ctime.getTime();
93 | } else {
94 | attributes.changeTime = ctime;
95 | }
96 | }
97 |
98 | if (SAVE_ACCESSTIME) {
99 | var atime = stats.atime;
100 | if (atime) {
101 | if (atime.getFullYear() >= 1970) {
102 | attributes.accessTime = atime.getTime();
103 | } else {
104 | attributes.accessTime = atime;
105 | }
106 | }
107 | }
108 |
109 | var birthtime = stats.birthtime;
110 | if (birthtime && (!mtime || birthtime.getTime() < mtime.getTime())) {
111 | // birthtime can be after mtime ??? OS problem ???
112 |
113 | if (birthtime.getFullYear() >= 1970) {
114 | attributes.birthTime = birthtime.getTime();
115 | } else {
116 | attributes.birthTime = birthtime;
117 | }
118 | }
119 | }
120 |
121 | /**
122 | *
123 | */
124 | newFile(parentNode, contentURL, upnpClass, stats, attributes, before, callback) {
125 |
126 | assert(parentNode instanceof Node, "Invalid parentNode parameter");
127 | assert(contentURL, "Invalid contentURL parameter");
128 | assert.equal(typeof (callback), "function", "Invalid callback parameter");
129 |
130 | var name=stats && stats.name;
131 | if (!name) {
132 | name = contentURL.basename;
133 | }
134 |
135 | attributes = attributes || {};
136 |
137 | var newNode = (upnpClass) => {
138 | if (!upnpClass) {
139 | callback({
140 | code : Repository.UPNP_CLASS_UNKNOWN
141 | });
142 | return;
143 | }
144 |
145 | this.service.newNode(parentNode, name, upnpClass, attributes, (node) => {
146 | node.contentURL=contentURL;
147 | node.contentTime=stats.mtime.getTime();
148 | PathRepository.FillAttributes(node, stats);
149 |
150 | }, before, callback);
151 | };
152 |
153 | var processStats = (stats) => {
154 | if (upnpClass) {
155 | return newNode(upnpClass);
156 | }
157 |
158 | var fileInfos = {
159 | contentURL : contentURL,
160 | mimeType : attributes.mimeType || stats.mimeType,
161 | stats : stats
162 | };
163 |
164 | var upnpClasses = this.service.searchUpnpClass(fileInfos, (error, upnpClasses) => {
165 | if (error) {
166 | return callback(error);
167 | }
168 |
169 | if (upnpClasses && upnpClasses.length) {
170 | upnpClass = this.acceptUpnpClass(upnpClasses, fileInfos);
171 | }
172 |
173 | newNode(upnpClass);
174 | });
175 | };
176 |
177 | if (stats) {
178 | return processStats(stats);
179 | }
180 |
181 | contentURL.stat((error, stats) => {
182 | if (error) {
183 | return callback(error);
184 | }
185 |
186 | processStats(stats);
187 | });
188 | }
189 |
190 | /**
191 | *
192 | */
193 | newFolder(parentNode, contentURL, upnpClass, stats, attributes, before, callback) {
194 |
195 | switch (arguments.length) {
196 | case 3:
197 | callback = upnpClass;
198 | upnpClass = undefined;
199 | break;
200 | case 4:
201 | callback = stats;
202 | stats = undefined;
203 | break;
204 | case 5:
205 | callback = attributes;
206 | attributes = undefined;
207 | break;
208 | case 6:
209 | callback = before;
210 | before = undefined;
211 | break;
212 | }
213 |
214 | assert(parentNode instanceof Node, "Invalid parentNode parameter");
215 | assert(contentURL, "Invalid contentURL parameter");
216 | assert.equal(typeof (callback), "function", "Invalid callback parameter");
217 |
218 | var name=stats && stats.name;
219 | if (!name) {
220 | name = contentURL.basename;
221 | }
222 |
223 | attributes = attributes || {};
224 | // attributes.contentURL = contentURL;
225 |
226 | var newNode = (upnpClass) => {
227 | this.service.newNode(parentNode, name, upnpClass, attributes, (node) => {
228 | node.contentURL=contentURL;
229 | node.contentTime=stats.mtime.getTime();
230 | PathRepository.FillAttributes(node, stats);
231 |
232 | }, before, callback);
233 | };
234 |
235 | var processStats = (stats) => {
236 | if (upnpClass) {
237 | return newNode(upnpClass);
238 | }
239 |
240 | var fileInfos = {
241 | contentURL : contentURL,
242 | mimeType : "inode/directory",
243 | stats : stats
244 | };
245 |
246 | this.service.searchUpnpClass(fileInfos, (error, upnpClasses) => {
247 | if (error) {
248 | return callback(error);
249 | }
250 |
251 | if (upnpClasses && upnpClasses.length) {
252 | upnpClass = this.acceptUpnpClass(upnpClasses, fileInfos);
253 | }
254 |
255 | newNode(upnpClass);
256 | });
257 | };
258 |
259 | if (stats) {
260 | return processStats(stats);
261 | }
262 |
263 | contentURL.stat((error, stats) => {
264 | if (error) {
265 | return callback(error);
266 | }
267 |
268 | processStats(stats);
269 | });
270 | }
271 | }
272 |
273 | module.exports = PathRepository;
274 |
275 | function computeDate(t) {
276 | if (t.getFullYear() >= 1970) {
277 | return t.getTime();
278 | }
279 |
280 | return t;
281 | }
282 |
--------------------------------------------------------------------------------
/lib/repositories/repository.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus:true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const Uuid = require('uuid');
6 | const Async = require('async');
7 |
8 | const debug = require('debug')('upnpserver:repositories');
9 | const logger = require('../logger');
10 |
11 | const Node = require('../node');
12 | const UpnpContainer = require('../class/object.container');
13 |
14 | class Repository {
15 | constructor(mountPath, configuration) {
16 | assert.equal(typeof (mountPath), "string", "Invalid mountPath parameter");
17 |
18 | this._configuration=configuration || {};
19 |
20 | if (!mountPath) {
21 | mountPath = "";
22 | }
23 | if (mountPath.charAt(0) !== '/') {
24 | mountPath = "/" + mountPath;
25 | }
26 |
27 | this._mountPath = mountPath;
28 | }
29 |
30 | get configuration() {
31 | return this._configuration;
32 | }
33 |
34 | get hashKey() {
35 | return {
36 | type: this.type,
37 | mountPath: this.mountPath
38 | };
39 | }
40 |
41 | get type() {
42 | throw new Error("Not implemented !");
43 | }
44 |
45 | /**
46 | *
47 | */
48 | get id() {
49 | return this._id;
50 | }
51 |
52 | /**
53 | *
54 | */
55 | get mountPath() {
56 | return this._mountPath;
57 | }
58 |
59 | /**
60 | *
61 | */
62 | get service() {
63 | if (!this.contentDirectoryService) {
64 | throw new Error("Not yet initialized");
65 | }
66 | return this.contentDirectoryService;
67 | }
68 |
69 | /**
70 | *
71 | */
72 | initialize(service, callback) {
73 | this.contentDirectoryService = service;
74 |
75 | debug("initialize", "Initialize repository",this.id);
76 |
77 | this._allocateNodesForPath(this.mountPath, (error, node) => {
78 | if (error) {
79 | return callback(error);
80 | }
81 |
82 | this._installListeners(service);
83 |
84 | callback(null, node);
85 | });
86 | }
87 |
88 | /**
89 | *
90 | */
91 | _installListeners(service) {
92 | if (this.browse) {
93 | service.asyncOn("browse", (list, node, options, callback) => this.browse(list, node, options, callback));
94 | }
95 |
96 | if (this.update) {
97 | service.asyncOn("update", (node, callback) => this.update(node, callback));
98 | }
99 | }
100 |
101 | /*
102 | * Repository.prototype.browse = function(list, node, callback) { return callback(null); };
103 | */
104 | /*
105 | * Repository.prototype.update = function(node, callback) { return callback(null); };
106 | */
107 |
108 |
109 | /**
110 | *
111 | */
112 | acceptUpnpClass(upnpClasses, fileInfos) {
113 | if (!upnpClasses) {
114 | return null;
115 | }
116 |
117 | var found= upnpClasses.find((up) => {
118 | var upnpClass = up.upnpClass;
119 |
120 | if (typeof (upnpClass.acceptFile) !== "function") {
121 | return false;
122 | }
123 |
124 | return upnpClass.acceptFile(fileInfos);
125 | });
126 | if (!found) {
127 | return null;
128 | }
129 |
130 | return found.upnpClass;
131 | }
132 |
133 | /**
134 | *
135 | */
136 | newVirtualContainer(parentNode, name, upnpClass, attributes, before, callback) {
137 |
138 | switch (arguments.length) {
139 | case 3:
140 | callback = upnpClass;
141 | upnpClass = undefined;
142 | break;
143 | case 4:
144 | callback = attributes;
145 | attributes = undefined;
146 | break;
147 | case 5:
148 | callback = before;
149 | before = undefined;
150 | break;
151 | }
152 |
153 | debug("initialize", "newVirtualContainer parentNode=#", parentNode.id, "name=", name,
154 | "upnpClass=", upnpClass, "attributes=", attributes);
155 |
156 | assert(parentNode instanceof Node, "Invalid parentNode parameter");
157 | assert(typeof (name) === "string", "Invalid name parameter");
158 | assert(typeof (callback) === "function", "Invalid callback parameter");
159 |
160 | attributes = attributes || {};
161 |
162 | upnpClass = upnpClass || UpnpContainer.UPNP_CLASS;
163 |
164 | this.service.newNode(parentNode, name, upnpClass, attributes, (node) => {
165 |
166 | }, before,
167 | (error, newNode, newNodeId) => {
168 | if (error) {
169 | var ex = new Error("newNode error");
170 | //logger.error("Can not create new node '"+name+"' to #"+parentNode.id);
171 | ex.parentNode=parentNode;
172 | ex.nodeName = name;
173 | ex.upnpClass = upnpClass;
174 | ex.error = error;
175 | return callback(ex);
176 | }
177 |
178 | debug("initialize", "NewNode created #", newNodeId, "=", newNode.attributes, "error=", error);
179 |
180 | callback(null, newNode, newNodeId);
181 | });
182 | }
183 |
184 | /**
185 | *
186 | */
187 | newNodeRef(parentNode, targetNode, name, initCallback, before, callback) {
188 |
189 | switch (arguments.length) {
190 | case 3:
191 | callback = name;
192 | name = undefined;
193 | break;
194 | case 4:
195 | callback = initCallback;
196 | initCallback = undefined;
197 | break;
198 | case 5:
199 | callback = before;
200 | before = undefined;
201 | break;
202 | }
203 |
204 | assert(parentNode instanceof Node, "Invalid parentNode parameter");
205 | assert(targetNode instanceof Node, "Invalid targetNode parameter");
206 | assert(typeof (callback) === "function", "Invalid callback parameter");
207 |
208 | this.service.newNodeRef(parentNode, targetNode, name, initCallback, before,
209 | callback);
210 | }
211 |
212 | /**
213 | *
214 | */
215 | _allocateNodesForPath(path, callback) {
216 |
217 | var ps = path.split("/");
218 | ps.shift(); // Path must start with /, remove empty string first element
219 |
220 | debug("_allocateNodesForPath", "allocate path=", path, "segments=", ps);
221 |
222 | var root=this.service.root;
223 |
224 | if (ps.length < 1 || !ps[0]) {
225 | return callback(null, root);
226 | }
227 |
228 | Async.reduce(ps, root, (parentNode, segment, callback) => {
229 |
230 | parentNode.getFirstVirtualChildByTitle(segment, (error, node) => {
231 | if (error) {
232 | return callback(error);
233 | }
234 |
235 | if (node) {
236 | debug("_allocateNodesForPath", "segment=", segment, "in", parentNode.id,
237 | "=>", node.id);
238 |
239 | return callback(null, node);
240 | }
241 |
242 | debug("_allocateNodesForPath", "segment=", segment, "in", parentNode.id, "=> NONE");
243 |
244 | this.newVirtualContainer(parentNode, segment, callback);
245 | });
246 | }, callback);
247 | }
248 |
249 | toString() {
250 | return "[Repository id="+this.id+" mountPath="+this.mountPath+"]";
251 | }
252 | }
253 |
254 | Repository.UPNP_CLASS_UNKNOWN = "UpnpClassUnknown";
255 |
256 | module.exports = Repository;
257 |
258 |
--------------------------------------------------------------------------------
/lib/repositories/scanner.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus:true, nomen: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const Util = require('util');
6 | const Async = require('async');
7 | const Path = require('path');
8 |
9 | const debug = require('debug')('upnpserver:repositories:Scanner');
10 |
11 | const PathRepository = require('./path');
12 | const logger = require('../logger');
13 |
14 | const FILES_PROCESSOR_LIMIT = 4;
15 | const FOLDER_SCAN_LIMIT = 4;
16 | const DIRECTORY_SCAN_LIMIT = 2;
17 |
18 | const SCAN_WAITING_MS = 1000 * 60;
19 |
20 | class ScannerRepository extends PathRepository {
21 |
22 | /**
23 | *
24 | */
25 | initialize(service, callback) {
26 | var log = false;
27 |
28 | var scan = (node) => {
29 | var dt=Date.now();
30 |
31 | this.scan(service, node, (error) => {
32 | if (error) {
33 | logger.error("ScannerRepository: Scan error for node #", node.id, "error=", error);
34 | return;
35 | }
36 |
37 | var s=Math.floor((Date.now()-dt)/1000);
38 |
39 | logger.info(`Scan of repository ${this._directoryURL} has been finished in ${s} second${(s>1)?"s":""}`);
40 |
41 | if (!log) {
42 | return;
43 | }
44 |
45 | node.treeString((error, string) => {
46 | if (error) {
47 | logger.error("ScannerRepository: Tree string error", error);
48 | return;
49 | }
50 | logger.debug(string);
51 | });
52 | });
53 |
54 | };
55 |
56 | super.initialize(service, (error, node) => {
57 | if (error) {
58 | return callback(error);
59 | }
60 |
61 | setImmediate(() => scan(node));
62 |
63 | callback(null, node);
64 | });
65 | }
66 |
67 | /**
68 | *
69 | */
70 | scan(service, node, callback) {
71 | assert(typeof(callback)==="function", "Invalid callback parameter");
72 |
73 | var files = [];
74 |
75 | var infos = {
76 | contentURL : this.directoryURL,
77 | node : node
78 | };
79 |
80 | this._scanDirectory(node, infos, files, (error) => {
81 | if (error) {
82 | var ex=new Error("Can not scan directory");
83 | //logger.error("Scan directory error", error);
84 | ex.node = node;
85 | ex.infos = infos;
86 | ex.files = files;
87 | ex.error = error;
88 | return callback(ex);
89 | }
90 |
91 | debug("scan", "Number of files to process : path=" , infos.contentURL, "count=",files.length);
92 |
93 | Async.eachLimit(files, FILES_PROCESSOR_LIMIT, (infos, callback) => {
94 | debug("scan", "Process file :",infos.contentURL);
95 |
96 | this.processFile(node, infos, (error) => {
97 | if (error) {
98 | logger.error("Process file node=#" + node.id + " infos=", infos,
99 | " error=", error);
100 | }
101 |
102 | setImmediate(callback);
103 | });
104 |
105 | }, (error) => {
106 |
107 | if (error) {
108 | var ex=new Error("Files processor error");
109 | //logger.error("Error while scaning files ", error);
110 | ex.files = files;
111 | ex.node = node;
112 | ex.infos = infos;
113 | ex.error = error;
114 | return callback(error);
115 | }
116 |
117 | debug("scan", files.length, "files processed");
118 |
119 | setImmediate(callback);
120 | });
121 | });
122 | }
123 |
124 | /**
125 | *
126 | */
127 | _scanDirectory(rootNode, parentInfos, files, callback) {
128 |
129 | debug("_scanDirectory", "Scan directory", parentInfos.contentURL);
130 |
131 | assert(parentInfos, "Parent infos is null");
132 | assert(parentInfos.contentURL, "ContentURL of Parent infos is undefined");
133 |
134 | parentInfos.contentURL.readdir((error, list) => {
135 | if (error) {
136 | error.contentURL=parentInfos.contentURL;
137 | //logger.warn("Error while reading directory ", parentInfos.contentURL, error);
138 | return callback(error);
139 | }
140 |
141 | var directories = [];
142 | Async.eachLimit(list, FOLDER_SCAN_LIMIT, (url, callback) => {
143 |
144 | url.stat((error, stats) => {
145 | if (error) {
146 | logger.error("Error while stat of", url, error);
147 | return callback(null, list);
148 | }
149 |
150 | // logger.debug("Scan item ", p);
151 |
152 | var infos = {
153 | contentURL : url,
154 | stats : stats,
155 | mimeType : stats.mimeType,
156 |
157 | parentInfos : parentInfos
158 | };
159 |
160 | if (stats.isDirectory()) {
161 | if (this.keepDirectory(infos)) {
162 | directories.push(infos);
163 | }
164 | return callback(null);
165 | }
166 |
167 | if (stats.isFile()) {
168 |
169 | if (this.keepFile(infos)) {
170 | // logger.debug("Keep file ", p);
171 | files.push(infos);
172 | }
173 |
174 | return callback(null);
175 | }
176 |
177 | callback(null);
178 | });
179 |
180 | }, (error) => {
181 | if (error) {
182 | var ex = new Error("Readdir error (url="+parentInfos.contentURL+")");
183 | ex.contentURL=parentInfos.contentURL;
184 | ex.list = list;
185 | ex.error = error;
186 | //logger.error("Reduce error", error);
187 | return callback(ex);
188 | }
189 |
190 | debug("_scanDirectory", "Directories length=",directories.length);
191 |
192 | if (!directories.length) {
193 | return callback(null);
194 | }
195 |
196 | Async.eachLimit(directories, DIRECTORY_SCAN_LIMIT, (directoryInfos, callback) => {
197 |
198 | debug("_scanDirectory", "Scan subdirectory", directoryInfos.contentURL, "files.count=",files.count);
199 |
200 | this.processDirectory(rootNode, directoryInfos, files, (error) => {
201 | if (error) {
202 | return callback(error);
203 | }
204 |
205 | setImmediate(callback);
206 | });
207 | }, callback);
208 | });
209 | });
210 | }
211 |
212 | /**
213 | *
214 | */
215 | keepFile(infos) {
216 | return false;
217 | }
218 |
219 | /**
220 | *
221 | */
222 | keepDirectory (infos) {
223 | return true;
224 | }
225 |
226 | /**
227 | *
228 | */
229 | processFile(node, infos, callback) {
230 | debug("processFile", "nothing to do ?");
231 | callback("Nothing to process ?");
232 | }
233 |
234 | /**
235 | *
236 | */
237 | processDirectory(rootNode, directoryInfos, files, callback) {
238 | this._scanDirectory(rootNode, directoryInfos, files, callback);
239 | }
240 | }
241 | module.exports = ScannerRepository;
242 |
--------------------------------------------------------------------------------
/lib/repositories/virtual.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debug = require('debug')('upnpserver:repositories:Virtual');
5 |
6 | const Repository = require('./repository');
7 |
8 | class VirtualRepository extends Repository {
9 |
10 | /**
11 | *
12 | */
13 | browse(list, node, callback) {
14 |
15 | this.mountNode = node;
16 |
17 | var searchClasses=this.configuration.searchClasses;
18 | if (searchClasses) {
19 | searchClasses.forEach((sc) => {
20 | node.addSearchClass(sc.name, sc.includeDerived);
21 | });
22 | }
23 |
24 | callback();
25 | }
26 |
27 | /**
28 | *
29 | */
30 | newFile(parentNode, path, upnpClass, stats, attributes, before, callback) {
31 | parentNode = parentNode || this.mountNode;
32 |
33 | super.newFile(parentNode, path, upnpClass, stats, attributes, before, callback);
34 | }
35 |
36 | /**
37 | *
38 | */
39 | newFolder(parentNode, path, upnpClass, stats, attributes, before, callback) {
40 | parentNode = parentNode || this.mountNode;
41 |
42 | switch (arguments.length) {
43 | case 3:
44 | callback = upnpClass;
45 | upnpClass = undefined;
46 | break;
47 | case 4:
48 | callback = stats;
49 | stats = undefined;
50 | break;
51 | case 5:
52 | callback = attributes;
53 | attributes = undefined;
54 | break;
55 | case 6:
56 | callback = before;
57 | before = undefined;
58 | break;
59 | }
60 |
61 | super.newFolder(parentNode, path, upnpClass, stats, attributes, before, callback);
62 | }
63 |
64 | /**
65 | *
66 | */
67 | newVirtualContainer(parentNode, name, upnpClass, attributes, before, callback) {
68 | parentNode = parentNode || this.mountNode;
69 |
70 | switch (arguments.length) {
71 | case 3:
72 | callback = upnpClass;
73 | upnpClass = undefined;
74 | break;
75 | case 4:
76 | callback = attributes;
77 | attributes = undefined;
78 | break;
79 | case 5:
80 | callback = before;
81 | before = undefined;
82 | break;
83 | }
84 |
85 | super.newVirtualContainer(parentNode, name, upnpClass, attributes, before, callback);
86 | }
87 | }
88 |
89 | module.exports = VirtualRepository;
90 |
--------------------------------------------------------------------------------
/lib/repositories/whatsNewRepository.js:
--------------------------------------------------------------------------------
1 | /* Organisation:
2 | Today and yesterday
3 | Last week
4 | Last month
5 | Last year
6 |
7 | + Structure
8 | today / Movies
9 | / Music
10 | / Photos
11 |
12 | */
13 |
--------------------------------------------------------------------------------
/lib/stateVar.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus:true, nomen: true, vars: true */
2 | "use strict";
3 |
4 | var debug = require("debug")("upnpserver:statevar:event");
5 |
6 | /*******************************************************************************************************************************
7 | * StateVar : implements evented and moderated stateVars getters and setters.
8 | *
9 | * @service : service, the service instance this var belongs to
10 | * @name : string, name of the state variable
11 | * @value : mixed, default value of the variable
12 | * @ns : string, xmlns:dt for vendor variable
13 | * @evented : boolean, send event on change
14 | * @moderation_rate : float, minimum delay in second allowed between two events, enable moderation when set
15 | * @additionalProps : array of string, statevar name to be sent with this event
16 | * (not realy in specs, allow event grouping)
17 | * @pre/post EventCb : function, callback executed before / after sending event
18 | *
19 | */
20 |
21 | var StateVar = module.exports = function(service, name, type, value, ns,
22 | evented, moderation_rate, additionalProps, preEventCb, postEventCb) {
23 |
24 | var self = this;
25 |
26 | if (value !== undefined) {
27 | self.value = value;
28 |
29 | } else {
30 | switch (type) {
31 | case "boolean":
32 | case "i4":
33 | case "iu4":
34 | case "iu2":
35 | case "i2":
36 | case "int":
37 | self.value = 0;
38 | break;
39 |
40 | // case "string":
41 | default:
42 | self.value = "";
43 | }
44 |
45 | }
46 |
47 | self.name = name;
48 | self.ns = ns;
49 | self.type = type;
50 | self.service = service;
51 |
52 | self.additionalProps = additionalProps || [];
53 | self.postEventCb = postEventCb;
54 | self.preEventCb = preEventCb;
55 |
56 | // implements set method
57 | if (evented && moderation_rate) {
58 | self.set = function(val) {
59 | var old = self.value;
60 | self.value = val;
61 | if (old !== val) {
62 | self.moderate();
63 | }
64 | };
65 |
66 | } else if (evented) {
67 | self.set = function(val) {
68 | var old = self.value;
69 | self.value = val;
70 | if (old !== val) {
71 | self.notify();
72 | }
73 | };
74 | } else {
75 | self.set = function(val) {
76 | self.value = val;
77 | };
78 | }
79 |
80 | self.rate = moderation_rate && 1000 * moderation_rate;
81 | self.next = moderation_rate && Date.now();
82 | self.wait = false;
83 | };
84 |
85 | /**
86 | * Push event xml e:property to a xmlContent array
87 | */
88 | StateVar.prototype.pushEventJXML = function(where) {
89 |
90 | var dt = {
91 | "dt:dt" : this.type
92 | };
93 | if (this.ns) {
94 | // s-l : handle xmlns
95 | for ( var xmlns in this.ns) {
96 | dt["xmlns:" + xmlns] = this.ns[xmlns];
97 | }
98 | }
99 | where.push({
100 | _name : "e:property",
101 | _content : {
102 | _name : "s:" + this.name,
103 | _attrs : dt,
104 | _content : this.value
105 | }
106 | });
107 | };
108 |
109 | StateVar.prototype.get = function() {
110 | return this.value;
111 | };
112 |
113 | StateVar.prototype.notify = function() {
114 | var self = this;
115 | if (debug.enabled) {
116 | debug("notify " + this.name);
117 | }
118 |
119 | if (self.preEventCb) {
120 | self.preEventCb();
121 | }
122 |
123 | var service = self.service;
124 | var stateVars = service.stateVars;
125 |
126 | var xmlProps = [];
127 |
128 | self.additionalProps.forEach(function(name) {
129 | stateVars[name].pushEventJXML(xmlProps);
130 | });
131 |
132 | self.pushEventJXML(xmlProps);
133 |
134 | service.makeEvent(xmlProps);
135 |
136 | if (self.postEventCb) {
137 | self.postEventCb();
138 | }
139 | };
140 |
141 | StateVar.prototype.moderate = function() {
142 | var self = this;
143 | var now = Date.now();
144 | if (now > self.next) {
145 | if (debug.enabled) {
146 | debug("emit moderate " + this.name);
147 | }
148 |
149 | self.next = now + self.rate;
150 | self.notify();
151 |
152 | setTimeout(function() {
153 | if (debug.enabled) {
154 | debug("stop moderate " + self.name);
155 | }
156 | self.wait = false;
157 | }, self.rate);
158 | self.wait = true;
159 | return;
160 | }
161 |
162 | if (self.wait) {
163 | return;
164 | }
165 |
166 | if (debug.enabled) {
167 | debug("start moderate " + this.name);
168 | }
169 |
170 | self.next = now + self.rate;
171 | self.notify();
172 | self.wait = true;
173 | };
174 |
--------------------------------------------------------------------------------
/lib/util/alphaNormalizer.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const debug= require('debug')('upnpserver:util:AlphaNormalizer');
5 | const logger = require('../logger');
6 |
7 |
8 | const ACCENTS_MAPPER = [ /[áãàâäåāăąǎǟǡǻ]/g, 'a', /[çćĉċč]/g, 'c', /[ďđ]/g, 'd',
9 | /[éèêëēĕėęěǝǯ]/g, 'e', /[ĝğġģǥǧǵ]/g, 'g', /[ĥħ]/g, 'h', /[íìîïĩīĭįıǐ]/g,
10 | 'i', /[ĵǰ]/g, 'j', /[ķǩ]/g, 'k', /[ĺļľŀł]/g, 'l', /[ñńņňʼnŋǹ]/g, 'n',
11 | /[óõòôöōŏőǒǫǭǿ]/g, 'o', /[ŕŗř]/g, 'r', /[śŝşš]/g, 's', /[ţťŧ]/g, 't',
12 | /[úùûüµǔǖǘǚǜ]/g, 'u', /[ýÿ]/g, 'y', /[źżžƶ]/g, 'z', /[œ]/g, 'oe', /[æǽǣ]/g,
13 | 'ae', /[ij]/g, 'ij', /[dzdž]/g, 'dz', /[lj]/g, 'lj', /[nj]/g, 'nj' ];
14 |
15 | class AlphaNormalizer {
16 |
17 | static normalize(s) {
18 | if (typeof (s) !== "string") {
19 | return s;
20 | }
21 | s = s.toLowerCase().trim();
22 |
23 | for (var i = 0; i < ACCENTS_MAPPER.length;) {
24 | var expr = ACCENTS_MAPPER[i++];
25 | var code = ACCENTS_MAPPER[i++];
26 |
27 | s = s.replace(expr, code);
28 | }
29 |
30 | return s;
31 | }
32 | }
33 |
34 | module.exports = AlphaNormalizer;
35 |
36 |
--------------------------------------------------------------------------------
/lib/util/asyncEventEmitter.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const debug = require('debug')('upnpserver:AsyncEventEmitter');
6 | const logger = require('../logger');
7 |
8 | const Async = require('async');
9 | const EventEmitter = require('events').EventEmitter;
10 | const Util = require('util');
11 |
12 | class AsyncEventEmitter extends EventEmitter {
13 | constructor() {
14 | super();
15 |
16 | this._eventsByName = {};
17 | this._asyncMaxListeners = 10;
18 | this._defaultPriority = 50;
19 | }
20 |
21 | setMaxListeners(n) {
22 | this._asyncMaxListeners = isNaN(n) ? 10 : n;
23 | return this;
24 | }
25 |
26 | listeners(name) {
27 | var eventsByName = this._eventsByName;
28 |
29 | var events = eventsByName[name];
30 | if (!events) {
31 | events = [];
32 | eventsByName[name] = events;
33 | }
34 |
35 | return events;
36 | }
37 |
38 | asyncOn(name, func, priority) {
39 |
40 | var l = this.listeners(name);
41 |
42 | if (typeof (func) !== 'function') {
43 | throw new Error('The event listener MUST be a function. You passed in a ' + typeof func);
44 | }
45 |
46 | if (l.length >= this._asyncMaxListeners) {
47 | logger.error('Error: Too many listeners!! This may be a bug in your code');
48 | }
49 |
50 | priority = (typeof (priority) === 'number') ? priority : this._defaultPriority;
51 | l.push({
52 | priority: priority,
53 | func: func
54 | });
55 |
56 | // Highest priority called first !
57 | l.sort((f1, f2) => {
58 | return f2.priority - f1.priority;
59 | });
60 |
61 | this.emit('newAsyncListener', name, func);
62 |
63 | return this;
64 | }
65 |
66 | asyncOnce(name, func, priority) {
67 |
68 | var fired = false;
69 | var onceFunc = () => {
70 | this.asyncRemoveListener(name, func);
71 |
72 | if (fired) {
73 | return;
74 | }
75 | fired = true;
76 |
77 | func.apply(this, arguments);
78 | };
79 |
80 | this.asyncOn(name, onceFunc, priority);
81 | return this;
82 | }
83 |
84 | asyncRemoveListener(name, func) {
85 | var l = this.listeners(name);
86 |
87 | for (var i = 0; i < l.length; i++) {
88 | if (l[i] !== func) {
89 | continue;
90 | }
91 |
92 | l.splice(i, 1);
93 |
94 | this.emit('removeAsyncListener', name, func);
95 | break;
96 | }
97 |
98 | return this;
99 | }
100 |
101 | hasListeners(name) {
102 | var l = this._eventsByName[name];
103 | if (!l || !l.length) {
104 | return false;
105 | }
106 |
107 | return true;
108 | }
109 |
110 | /**
111 | *
112 | * @param {string} name
113 | * @param {*} [x] arguments
114 | * @param {Function} xcallback
115 | */
116 | asyncEmit(name, x, xcallback) {
117 | assert(typeof(name) === "string", "Invalid name parameter");
118 |
119 | var callback = arguments[arguments.length - 1];
120 | assert(typeof(callback) === "function", "Invalid callback parameter");
121 |
122 | var l = this.listeners(name);
123 |
124 | if (!l || !l.length) {
125 |
126 | debug("asyncEmit", "Emit name=", name, " EMPTY list");
127 |
128 | return callback();
129 | }
130 |
131 | var args = Array.prototype.slice.call(arguments, 1, arguments.length - 1);
132 | var argsLength = args.length;
133 |
134 | debug("asyncEmit", "Emit name=", name, "l=", l); //,"args=",args);
135 |
136 | var errors = [];
137 | Async.eachSeries(l, (listener, callback) => {
138 | args[argsLength] = (error) => {
139 | if (error) {
140 | logger.error("Call of listener returns ", error);
141 | errors.push(error);
142 | }
143 |
144 | callback();
145 | };
146 |
147 | debug("asyncEmit", "Call listener=", listener); //, "args=",args);
148 |
149 | listener.func.apply(this, args);
150 |
151 | }, () => {
152 | debug("asyncEmit", "End of name=", name, "errors=", errors);
153 |
154 | if (errors && errors.length) {
155 | return callback(errors);
156 | }
157 |
158 | setImmediate(callback);
159 | });
160 | }
161 | }
162 |
163 | module.exports = AsyncEventEmitter;
164 |
--------------------------------------------------------------------------------
/lib/util/errorSoap.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, plusplus:true, nomen: true, vars: true */
2 | "use strict";
3 |
4 | var ErrorSoap = {
5 | 401 : "Invalid Action",
6 | 402 : "Invalid Args",
7 | 404 : "Invalid Var",
8 | 501 : "Action Failed",
9 | 600 : "Argument Value Invalid",
10 | 601 : "Argument Value Out of Range",
11 | 602 : "Optional Action Not Implemented",
12 | 604 : "Human Intervention Required",
13 | 605 : "String Argument Too Long",
14 | 701 : "No Such Object",
15 | 709 : "Invalid Sort Criteria",
16 | 710 : "No such container"
17 | };
18 |
19 | ErrorSoap.soap = function(code) {
20 | code = code || 500;
21 | var msg = this[code] || 'Unknown error';
22 | var err = new Error(msg);
23 | err.code = code;
24 | return err;
25 | };
26 |
27 | module.exports = ErrorSoap;
--------------------------------------------------------------------------------
/lib/util/jstoxml.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | var toXML = function(obj, config){
5 | // include XML header
6 | config = config || {};
7 | var out = config.header ? '\n' : '';
8 |
9 | var origIndent = config.indent || '';
10 | var indent = '';
11 |
12 | var filter = function customFilter(txt) {
13 | if(!config.filter) return txt;
14 | var mappings = config.filter;
15 | var replacements = [];
16 | for(var map in mappings) {
17 | if(!mappings.hasOwnProperty(map)) continue;
18 | replacements.push(map);
19 | }
20 | return String(txt).replace(new RegExp('(' + replacements.join('|') + ')', 'g'), function(str, entity) {
21 | return mappings[entity] || '';
22 | });
23 | };
24 |
25 | // helper function to push a new line to the output
26 | var push = function(string){
27 | out += string + (origIndent ? '\n' : '');
28 | };
29 |
30 | /* create a tag and add it to the output
31 | Example:
32 | outputTag({
33 | name: 'myTag', // creates a tag
34 | indent: ' ', // indent string to prepend
35 | closeTag: true, // starts and closes a tag on the same line
36 | selfCloseTag: true,
37 | attrs: { // attributes
38 | foo: 'bar', // results in
39 | foo2: 'bar2'
40 | }
41 | });
42 | */
43 | var outputTag = function(tag){
44 | var attrsString = '';
45 | var outputString = '';
46 | var attrs = tag.attrs || '';
47 |
48 | // turn the attributes object into a string with key="value" pairs
49 | for(var attr in attrs){
50 | if(attrs.hasOwnProperty(attr)) {
51 | attrsString += ' ' + attr + '="' + filter(attrs[attr]) + '"';
52 | }
53 | }
54 |
55 | // assemble the tag
56 | outputString += (tag.indent || '') + '<' + (tag.closeTag ? '/' : '') +
57 | tag.name + (!tag.closeTag ? attrsString : '') + (tag.selfCloseTag ? '/' : '') + '>';
58 |
59 | // if the tag only contains a text string, output it and close the tag
60 | if(tag.text || tag.text === ''){
61 | outputString += filter(tag.text) + '' + tag.name + '>';
62 | }
63 |
64 | push(outputString);
65 | };
66 |
67 | // custom-tailored iterator for input arrays/objects (NOT a general purpose iterator)
68 | var every = function(obj, fn, indent){
69 | // array
70 | if(Array.isArray(obj)){
71 | obj.every(function(elt){ // for each element in the array
72 | fn(elt, indent);
73 | return true; // continue to iterate
74 | });
75 |
76 | return;
77 | }
78 |
79 | // object with tag name
80 | if(obj._name){
81 | fn(obj, indent);
82 | return;
83 | }
84 |
85 | // iterable object
86 | for(var key in obj){
87 | var type = typeof obj[key];
88 |
89 | if(obj.hasOwnProperty(key) && (obj[key] || type === 'boolean' || type === 'number')){
90 | fn({_name: key, _content: obj[key]}, indent);
91 | //} else if(!obj[key]) { // null value (foo:'')
92 | } else if(obj.hasOwnProperty(key) && obj[key] === null) { // null value (foo:null)
93 | fn(key, indent); // output the keyname as a string ('foo')
94 | } else if(obj.hasOwnProperty(key) && obj[key] === '') {
95 | // blank string
96 | outputTag({
97 | name: key,
98 | text: ''
99 | });
100 | }
101 | }
102 | };
103 |
104 | var convert = function convert(input, indent){
105 | var type = typeof input;
106 |
107 | if(!indent) indent = '';
108 |
109 | if(Array.isArray(input)) type = 'array';
110 |
111 | var path = {
112 | 'string': function(){
113 | push(indent + filter(input));
114 | },
115 |
116 | 'boolean': function(){
117 | push(indent + (input ? 'true' : 'false'));
118 | },
119 |
120 | 'number': function(){
121 | push(indent + input);
122 | },
123 |
124 | 'array': function(){
125 | every(input, convert, indent);
126 | },
127 |
128 | 'function': function(){
129 | push(indent + input());
130 | },
131 |
132 | 'object': function(){
133 | if(!input._name){
134 | every(input, convert, indent);
135 | return;
136 | }
137 |
138 | var outputTagObj = {
139 | name: input._name,
140 | indent: indent,
141 | attrs: input._attrs
142 | };
143 |
144 | var type = typeof input._content;
145 |
146 | if(type === 'undefined'){
147 | outputTagObj.selfCloseTag = true;
148 | outputTag(outputTagObj);
149 | return;
150 | }
151 |
152 | var objContents = {
153 | 'string': function(){
154 | outputTagObj.text = input._content;
155 | outputTag(outputTagObj);
156 | },
157 |
158 | 'boolean': function(){
159 | outputTagObj.text = (input._content ? 'true' : 'false');
160 | outputTag(outputTagObj);
161 | },
162 |
163 | 'number': function(){
164 | outputTagObj.text = input._content.toString();
165 | outputTag(outputTagObj);
166 | },
167 |
168 | 'object': function(){ // or Array
169 | outputTag(outputTagObj);
170 |
171 | every(input._content, convert, indent + origIndent);
172 |
173 | outputTagObj.closeTag = true;
174 | outputTag(outputTagObj);
175 | },
176 |
177 | 'function': function(){
178 | outputTagObj.text = input._content(); // () to execute the fn
179 | outputTag(outputTagObj);
180 | }
181 | };
182 |
183 | if(objContents[type]) objContents[type]();
184 | }
185 |
186 | };
187 |
188 | if(path[type]) path[type]();
189 | };
190 |
191 | convert(obj, indent);
192 |
193 | return out;
194 | };
195 |
196 | exports.toXML = toXML;
--------------------------------------------------------------------------------
/lib/util/namedSemaphore.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | var debug = require('debug')('upnpserver:NamedSemaphore');
5 |
6 | var Semaphore = require('./semaphore');
7 | var NodeWeakHashmap = require('./nodeWeakHashmap');
8 |
9 | const MAP_TIMEOUT = 1000*30;
10 |
11 | class NamedSemaphore {
12 | constructor(name) {
13 | this._name = name;
14 | this._map=new NodeWeakHashmap(name, MAP_TIMEOUT);
15 | }
16 |
17 | get name() {
18 | return this._name;
19 | }
20 |
21 | take(name, callback) {
22 | var semaphore=this._map.get(name);
23 | if (semaphore) {
24 | semaphore.take(() => {
25 | callback(semaphore);
26 | });
27 | return;
28 | }
29 |
30 | semaphore = new Semaphore(this.name+":"+name);
31 | this._map.put({id: name}, semaphore);
32 |
33 | semaphore.take(() => {
34 | callback(semaphore);
35 | });
36 | }
37 | }
38 |
39 | module.exports = NamedSemaphore;
40 |
--------------------------------------------------------------------------------
/lib/util/nodeWeakHashmap.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const debug = require('debug')('upnpserver:weakmap');
6 |
7 | const logger = require('../logger');
8 |
9 |
10 | const DELAY_MS = 500;
11 | const MIN_DELAY_MS = 250;
12 | const MAX_READ_COUNT = 10000/DELAY_MS;
13 |
14 | class NodeWeakHashmap {
15 | constructor(name, delay, verifyUpdateId, garbageFunc) {
16 | this.name = name;
17 | this._delay = Math.max(delay || DELAY_MS, MIN_DELAY_MS);
18 | this._verifyUpdateId = !!verifyUpdateId;
19 | this._garbageFunc = garbageFunc;
20 |
21 | this._map = {};
22 | this._count = 0;
23 | }
24 |
25 | get(id, node) {
26 | if (!this._count) {
27 | return undefined;
28 | }
29 |
30 | var v = this._map[id];
31 | if (!v || (this._verifyUpdateId && node && v.updateId != node.updateId)) {
32 | return undefined;
33 | }
34 |
35 | v.readCount++;
36 | var d=(this._intervalId) ? this._now : (Date.now() + this._delay);
37 | if (v.readCount>1) {
38 | d += this._delay*Math.min(v.readCount-1, MAX_READ_COUNT);
39 | }
40 | v.date = d;
41 | return v.value;
42 | }
43 |
44 | put(node, value) {
45 | assert(value!==null && value!==undefined, "Invalid value");
46 |
47 | if (!this._intervalId) {
48 | this._now = Date.now() + this._delay;
49 |
50 | this._intervalId = setInterval(this._garbage.bind(this), this._delay);
51 |
52 | debug("put", "[", this.name, "] Start interval #" + this._intervalId);
53 | }
54 |
55 | var v = this._map[node.id];
56 | if (!v) {
57 | v = {};
58 | this._map[node.id] = v;
59 | this._count++;
60 | }
61 |
62 | v.date = this._now;
63 | v.value = value;
64 | v.readCount = 0;
65 |
66 | if (this._verifyUpdateId) {
67 | v.updateId = node.updateId;
68 | }
69 | }
70 |
71 | remove(node) {
72 | var k = node.id;
73 | var v = this._map[k];
74 | if (!v) {
75 | return;
76 | }
77 |
78 | delete this._map[node.id];
79 | this._count--;
80 |
81 | debug("remove", "[", this.name, "] Remove key", k);
82 |
83 | var garbageFunc = this._garbageFunc;
84 | if (garbageFunc) {
85 | try {
86 | garbageFunc(v.value, k, v);
87 |
88 | } catch (x) {
89 | logger.error("Exception while calling garbage function ", x, x.stack);
90 | }
91 | }
92 | }
93 |
94 |
95 | clear() {
96 | var map=this._map;
97 | this._map = {};
98 | this._count = 0;
99 |
100 | var garbageFunc = this._garbageFunc;
101 | if (garbageFunc) {
102 | for ( var k in map) {
103 | var v=map[k];
104 |
105 | try {
106 | garbageFunc(v.value, k, v);
107 |
108 | } catch (x) {
109 | logger.error("Exception while calling garbage function ", x, x.stack);
110 | }
111 | }
112 | }
113 |
114 | var iid = this._intervalId;
115 | debug("clear", "Stop interval #", iid);
116 |
117 | if (iid) {
118 | this._intervalId = undefined;
119 | clearInterval(iid);
120 | }
121 | }
122 |
123 | _garbage() {
124 | var now = Date.now();
125 | this._now = now + this._delay;
126 |
127 | var garbageFunc = this._garbageFunc;
128 | var map = this._map;
129 | var count = 0;
130 | var gs;
131 | for ( var k in map) {
132 | var v = map[k];
133 |
134 | if (v.date > now) {
135 | continue;
136 | }
137 |
138 | delete map[k];
139 | count++;
140 | this._count--;
141 |
142 | if (!garbageFunc) {
143 | continue;
144 | }
145 | if (!gs) {
146 | gs = [];
147 | }
148 | gs.push(v);
149 | }
150 |
151 | debug("_garbage", "[", this.name, "] Remove", count, "keys", this._count, "left");
152 |
153 | if (gs) {
154 | gs.forEach((v) => {
155 | try {
156 | garbageFunc(v.value, k, v);
157 |
158 | } catch (x) {
159 | logger.error("Exception while calling garbage function ", x, x.stack);
160 | }
161 |
162 | });
163 | }
164 |
165 | if (!this._count) {
166 | var iid = this._intervalId;
167 | debug("_garbage", "[", this.name, "] Stop interval #", iid);
168 |
169 | if (iid) {
170 | this._intervalId = undefined;
171 | clearInterval(iid);
172 | }
173 | }
174 | }
175 | }
176 |
177 | module.exports = NodeWeakHashmap;
178 |
--------------------------------------------------------------------------------
/lib/util/semaphore.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | var debug = require('debug')('upnpserver:Semaphore');
5 |
6 | class Semaphore {
7 |
8 | constructor(name) {
9 | this._name=name;
10 | }
11 |
12 | take(func) {
13 | //console.log("["+this._name+"] Take semaphore (taken="+this._taken+")");
14 | if (!this._taken) {
15 | this._taken=true;
16 | func(this);
17 | return;
18 | }
19 |
20 | if (!this._waitings) {
21 | this._waitings=[];
22 | }
23 |
24 | this._waitings.push(func);
25 | }
26 |
27 | leave() {
28 | if (!this._waitings || !this._waitings.length) {
29 | //console.log("["+this._name+"] Release semaphore EMPTY");
30 | this._taken=false;
31 | return;
32 | }
33 |
34 | var f=this._waitings.shift();
35 |
36 | //console.log("["+this._name+"] Release semaphore shift "+this._waitings);
37 |
38 | setImmediate(f.bind(this, this));
39 | }
40 |
41 | get current() {
42 | if (!this._taken) {
43 | return 0;
44 | }
45 |
46 | if (!this._waitings) {
47 | return 1;
48 | }
49 |
50 | return this._waitings.length+1;
51 | }
52 | }
53 |
54 | module.exports = Semaphore;
55 |
--------------------------------------------------------------------------------
/lib/util/url.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, esversion: 6 */
2 | "use strict";
3 |
4 | const assert = require('assert');
5 | const debug = require('debug')('upnpserver:URL');
6 | const Path = require('path');
7 |
8 | const Mime = require('mime');
9 |
10 | /**
11 | * @author Olivier Oeuillot
12 | */
13 | class URL {
14 |
15 | constructor(contentProvider, path) {
16 | assert(contentProvider, "Invalid contentProvider parameter");
17 | assert(typeof(path)==='string', "Invalid path parameter");
18 |
19 | Object.defineProperty(this, "contentProvider", {
20 | enumerable: false,
21 | configurable: false,
22 | writable: false,
23 | value: contentProvider
24 | });
25 |
26 | Object.defineProperty(this, "contentProviderName", {
27 | enumerable: true,
28 | configurable: false,
29 | writable: false,
30 | value: contentProvider.name
31 | });
32 |
33 | Object.defineProperty(this, "path", {
34 | enumerable: true,
35 | configurable: false,
36 | writable: false,
37 | value: path
38 | });
39 |
40 | }
41 |
42 | get basename() {
43 | return Path.posix.basename(this.path);
44 | }
45 |
46 | changeBasename(newBaseName) {
47 | return this.join('..', newBaseName);
48 | }
49 |
50 | join() {
51 | var args=_concatPath(this.path, arguments);
52 |
53 | var newURL = this.contentProvider.join.apply(this.contentProvider, args);
54 |
55 | return new URL(this.contentProvider, newURL);
56 | }
57 |
58 | stat() {
59 | var args=_concatPath(this.path, arguments);
60 |
61 | this.contentProvider.stat.apply(this.contentProvider, args);
62 | }
63 |
64 | createReadStream(session, options, callback) {
65 | this.contentProvider.createReadStream(session, this.path, options, callback);
66 | }
67 |
68 | createWriteStream(options, callback) {
69 | this.contentProvider.createWriteStream(this.path, options, callback);
70 | }
71 |
72 | readContent() {
73 | var args=_concatPath(this.path, arguments);
74 |
75 | debug("readContent", "parameters=",args);
76 |
77 | this.contentProvider.readContent.apply(this.contentProvider, args);
78 | }
79 |
80 | writeContent() {
81 | var args=_concatPath(this.path, arguments);
82 |
83 | debug("writeContent", "parameters=",args);
84 |
85 | this.contentProvider.writeContent.apply(this.contentProvider, args);
86 | }
87 |
88 | mimeLookup() {
89 | return Mime.lookup(this.basename);
90 | }
91 |
92 | readdir(callback) {
93 | this.contentProvider.readdir(this.path, callback);
94 | }
95 |
96 | toString() {
97 | var protocol = this.contentProvider.protocol;
98 |
99 | if (!protocol) {
100 | return this.path;
101 | }
102 | return protocol+":"+this.path;
103 | }
104 | }
105 |
106 | function _concatPath(path, args) {
107 | var a=[path];
108 | a.push.apply(a,args);
109 |
110 | return a;
111 | }
112 |
113 | module.exports = URL;
114 |
--------------------------------------------------------------------------------
/lib/util/xmlFilters.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Olivier Oeuillot
3 | */
4 |
5 | module.exports = {
6 | '<' : '<',
7 | '>' : '>',
8 | '"' : '"',
9 | '\'' : ''',
10 | '&' : '&'
11 | };
12 |
--------------------------------------------------------------------------------
/lib/util/xmldoc.js:
--------------------------------------------------------------------------------
1 | // xmldoc source from https://github.com/nfarina/xmldoc
2 |
3 | (function() {
4 |
5 | // global on the server, window in the browser
6 | var sax, root = this;
7 |
8 | if (typeof module !== 'undefined' && module.exports) {
9 | sax = require('sax');
10 | root = module.exports;
11 | } else {
12 | sax = root.sax;
13 | if (!sax) // no sax for you!
14 | throw new Error(
15 | "Expected sax to be defined. Make sure you're including sax.js before this file.");
16 | }
17 |
18 | /*
19 | * XmlElement is our basic building block. Everything is an XmlElement; even XmlDocument behaves like an XmlElement by
20 | * inheriting its attributes and functions.
21 | */
22 |
23 | function XmlElement(tag) {
24 | this.name = tag.name;
25 | this.attr = tag.attributes || {};
26 | this.val = "";
27 | this.uri = tag.uri;
28 | this.children = [];
29 |
30 | // console.log("Tag=", tag);
31 | }
32 |
33 | var _splitNameRegExp = /(xmlns)(:[a-z0-9_-]+)?/i;
34 | // SaxParser handlers
35 |
36 | XmlElement.prototype._openTag = function(tag) {
37 |
38 | var child = new XmlElement(tag);
39 |
40 | // add to our children array
41 | this.children.push(child);
42 |
43 | // update first/last pointers
44 | if (!this.firstChild)
45 | this.firstChild = child;
46 | this.lastChild = child;
47 |
48 | delegates.unshift(child);
49 |
50 | var xmlns;
51 | var attrs = tag.attributes;
52 |
53 | // console.log("Attributes="+attrs+"/"+attrs.length);
54 | if (attrs) {
55 | for ( var name in attrs) {
56 | var value = attrs[name];
57 |
58 | var r = _splitNameRegExp.exec(name);
59 |
60 | // console.log("Try attr "+name+"/"+value, r);
61 |
62 | if (!r) {
63 | continue;
64 | }
65 | if (!xmlns) {
66 | xmlns = {};
67 | for ( var k in this.namespaceURIs) {
68 | xmlns[k] = this.namespaceURIs[k];
69 | }
70 | }
71 |
72 | xmlns[(r[2] && r[2].slice(1)) || ""] = value.value;
73 | }
74 |
75 | if (xmlns) {
76 | // console.log("New namespaces ",xmlns," for "+tag.nodeName);
77 | }
78 | }
79 |
80 | child.namespaceURIs = xmlns || this.namespaceURIs;
81 | };
82 |
83 | XmlElement.prototype._closeTag = function() {
84 | delegates.shift();
85 | };
86 |
87 | XmlElement.prototype._text = function(text) {
88 | if (text)
89 | this.val += text;
90 | };
91 |
92 | XmlElement.prototype._cdata = function(cdata) {
93 | if (cdata)
94 | this.val += cdata;
95 | };
96 |
97 | // Useful functions
98 |
99 | XmlElement.prototype.eachChild = function(iterator, context) {
100 | for (var i = 0, l = this.children.length; i < l; i++) {
101 | if (iterator.call(context, this.children[i], i, this.children) === false) {
102 | return;
103 | }
104 | }
105 | };
106 |
107 | XmlElement.prototype.childNamed = function(name, xmlns) {
108 | for (var i = 0, l = this.children.length; i < l; i++) {
109 | var child = this.children[i];
110 |
111 | if (xmlns !== undefined) {
112 | // console.log("Compare "+xmlns+"/"+child.uri);
113 | if (child.uri !== xmlns) {
114 | continue;
115 | }
116 | }
117 |
118 | if (child.name === name)
119 | return child;
120 | }
121 | };
122 |
123 | XmlElement.prototype.childrenNamed = function(name) {
124 | var matches = [];
125 |
126 | for (var i = 0, l = this.children.length; i < l; i++)
127 | if (this.children[i].name === name)
128 | matches.push(this.children[i]);
129 |
130 | return matches;
131 | };
132 |
133 | XmlElement.prototype.childWithAttribute = function(name, value) {
134 | for (var i = 0, l = this.children.length; i < l; i++) {
135 | var child = this.children[i];
136 | if ((value && child.attr[name] === value) || (!value && child.attr[name]))
137 | return child;
138 | }
139 | };
140 |
141 | XmlElement.prototype.descendantWithPath = function(path) {
142 | var descendant = this;
143 | var components = path.split('.');
144 |
145 | for (var i = 0, l = components.length; i < l; i++)
146 | if (descendant)
147 | descendant = descendant.childNamed(components[i]);
148 | else
149 | return undefined;
150 |
151 | return descendant;
152 | };
153 |
154 | XmlElement.prototype.valueWithPath = function(path) {
155 | var components = path.split('@');
156 | var descendant = this.descendantWithPath(components[0]);
157 | if (descendant)
158 | return components.length > 1 ? descendant.attr[components[1]] : descendant.val;
159 | };
160 |
161 | // String formatting (for debugging)
162 |
163 | XmlElement.prototype.toString = function() {
164 | return this.toStringWithIndent("");
165 | };
166 |
167 | XmlElement.prototype.toStringWithIndent = function(indent) {
168 | var s = "";
169 | s += indent + "<" + this.name;
170 |
171 | for ( var name in this.attr)
172 | s += " " + name + '="' + this.attr[name] + '"';
173 |
174 | var trimVal = this.val.trim();
175 |
176 | if (trimVal.length > 25)
177 | trimVal = trimVal.substring(0, 25).trim() + "…";
178 |
179 | if (this.children.length) {
180 | s += ">\n";
181 |
182 | var childIndent = indent + " ";
183 |
184 | if (trimVal.length)
185 | s += childIndent + trimVal + "\n";
186 |
187 | for (var i = 0, l = this.children.length; i < l; i++)
188 | s += this.children[i].toStringWithIndent(childIndent) + "\n";
189 |
190 | s += indent + "" + this.name + ">";
191 | } else if (trimVal.length) {
192 | s += ">" + trimVal + "" + this.name + ">";
193 | } else
194 | s += "/>";
195 |
196 | return s;
197 | };
198 |
199 | /*
200 | * XmlDocument is the class we expose to the user; it uses the sax parser to create a hierarchy of XmlElements.
201 | */
202 |
203 | function XmlDocument(xml) {
204 |
205 | if (!xml) {
206 | throw new Error("No XML to parse!");
207 | }
208 |
209 | // console.log("xml=",xml);
210 |
211 | var parser = sax.parser(true, {
212 | xmlns : true
213 | }); // strict
214 |
215 | parser.onopentag = function() {
216 | var top = delegates[0];
217 | top._openTag.apply(top, arguments);
218 | };
219 | parser.onclosetag = function() {
220 | var top = delegates[0];
221 | top._closeTag.apply(top, arguments);
222 | };
223 | parser.ontext = function() {
224 | var top = delegates[0];
225 | top._text.apply(top, arguments);
226 | };
227 | parser.oncdata = function() {
228 | var top = delegates[0];
229 | top._cdata.apply(top, arguments);
230 | };
231 |
232 | // We'll use the file-scoped "delegates" var to remember what elements we're currently
233 | // parsing; they will push and pop off the stack as we get deeper into the XML hierarchy.
234 | // It's safe to use a global because JS is single-threaded.
235 | delegates = [ this ];
236 | this.namespaceURIs = {};
237 |
238 | parser.write(xml);
239 | }
240 |
241 | // make XmlDocument inherit XmlElement's methods
242 | extend(XmlDocument.prototype, XmlElement.prototype);
243 |
244 | XmlDocument.prototype._openTag = function(tag) {
245 | if (typeof this.children === 'undefined') {
246 | // the first tag we encounter should be the root - we'll "become" the root XmlElement
247 | XmlElement.call(this, tag);
248 | return;
249 | }
250 |
251 | // all other tags will be the root element's children
252 | XmlElement.prototype._openTag.apply(this, arguments);
253 | };
254 |
255 | // file-scoped global stack of delegates
256 | var delegates = null;
257 |
258 | /*
259 | * Helper functions
260 | */
261 |
262 | // a relatively standard extend method
263 | function extend(destination, source) {
264 | for ( var prop in source)
265 | if (source.hasOwnProperty(prop))
266 | destination[prop] = source[prop];
267 | }
268 |
269 | root.XmlDocument = XmlDocument;
270 |
271 | })();
--------------------------------------------------------------------------------
/lib/xmlns.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true, vars: true, nomen: true, sub: true */
2 | "use strict";
3 |
4 | module.exports = {
5 |
6 | DIDL_LITE : "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
7 |
8 | DLNA_DEVICE : "urn:schemas-dlna-org:device-1-0",
9 | DLNA_METADATA : "urn:schemas-dlna-org:metadata-1-0/",
10 |
11 | JASMIN_FILEMETADATA : "urn:schemas-jasmin-upnp.net:filemetadata/",
12 | JASMIN_MUSICMETADATA : "urn:schemas-jasmin-upnp.net:musicmetadata/",
13 | JASMIN_MOVIEMETADATA : "urn:schemas-jasmin-upnp.net:moviemetadata/",
14 |
15 | MICROSOFT_DATATYPES : "urn:schemas-microsoft-com:datatypes",
16 | MICROSFT_DEVICE_FOUNDATION: "http://schemas.microsoft.com/windows/2008/09/devicefoundation",
17 | MICROSOFT_WINDOWS_PNPX: "http://schemas.microsoft.com/windows/pnpx/2005/11",
18 |
19 | PURL_ELEMENT : "http://purl.org/dc/elements/1.1/",
20 |
21 | SEC_DLNA: "http://www.sec.co.kr/dlna",
22 |
23 | SOAP_ENVELOPE : "http://schemas.xmlsoap.org/soap/envelope/",
24 |
25 | UPNP_CONTENT_DIRECTORY_1 : "urn:schemas-upnp-org:service:ContentDirectory:1",
26 | UPNP_DEVICE : "urn:schemas-upnp-org:device-1-0",
27 | UPNP_EVENT : "urn:schemas-upnp-org:event-1-0",
28 | UPNP_METADATA : "urn:schemas-upnp-org:metadata-1-0/upnp/",
29 | UPNP_SERVICE : "urn:schemas-upnp-org:service-1-0",
30 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upnpserver",
3 | "version": "3.0.1",
4 | "description": "Simple Upnpserver server. It supports several audio tags (id3,...) and matroska metadatas.",
5 | "main": "api.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git@github.com:oeuillot/upnpserver.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/oeuillot/upnpserver/issues"
12 | },
13 | "engines": {
14 | "node": ">=5.0.0"
15 | },
16 | "dependencies": {
17 | "async": "latest",
18 | "debug": "latest",
19 | "exif": "latest",
20 | "follow-redirects": "0.0.3",
21 | "gm": "^1.21.1",
22 | "ip": "0.3",
23 | "jstoxml": "0.2",
24 | "matroska": "2.2.2",
25 | "mime": "1.2",
26 | "mkdirp": "latest",
27 | "moviedb": "^0.2.2",
28 | "music-metadata": "^0.7.14",
29 | "node-ssdp": "^2.7.0",
30 | "omdb-client": "^1.0.6",
31 | "range-parser": "^1.0.3",
32 | "request": "^2.69.0",
33 | "sax": "0.4.2",
34 | "send": "0.13",
35 | "status": "latest",
36 | "uuid": "^3.0.0"
37 | },
38 | "optionalDependencies": {
39 | "nedb": "^1.5.0",
40 | "mongodb": "^2.1.3"
41 | },
42 | "keywords": [
43 | "Node.js",
44 | "ushare",
45 | "mediatomb",
46 | "upnp"
47 | ],
48 | "author": "Olivier Oeuillot",
49 | "license": "GPL",
50 | "readmeFilename": "README.md",
51 | "devDependencies": {
52 | "jshint": "^2.6.0",
53 | "mocha": "~2.1.0"
54 | },
55 | "scripts": {
56 | "test": "./node_modules/.bin/jshint lib/"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/api.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert'),
2 | Api = require('../api');
3 |
4 | describe("API", function () {
5 | var api,
6 | expectedMusic,
7 | expectedPictures;
8 |
9 | beforeEach(function () {
10 | expectedMusic = [
11 | {
12 | "repositoryId": "path:/home/user",
13 | "mountPath": "/",
14 | "directoryPath": "/home/user",
15 | "searchClasses": undefined
16 | },
17 | {
18 | "repositoryId": "music:/home/user/music",
19 | "mountPath": "/Music",
20 | "directoryPath": "/home/user/music"
21 | }
22 | ];
23 |
24 | expectedPictures = [
25 | {
26 | "repositoryId": "path:/home/user",
27 | "mountPath": "/",
28 | "directoryPath": "/home/user",
29 | "searchClasses": undefined
30 | },
31 | {
32 | "repositoryId": "path:/home/user/pictures",
33 | "mountPath": "/Pictures",
34 | "directoryPath": "/home/user/pictures",
35 | "searchClasses": undefined
36 | }
37 | ];
38 |
39 | api = new Api({}, "/home/user");
40 | });
41 |
42 | it("Path as string should be valid", function () {
43 | api.initPaths("/home/user/pictures");
44 | expectedPictures[1].mountPath = "/";
45 |
46 | assert.deepEqual(api.directories, expectedPictures);
47 | });
48 |
49 | it("Path as object should be valid", function () {
50 | api.initPaths({
51 | path: "/home/user/music",
52 | mountPoint: "/Music",
53 | type: "music"
54 | });
55 |
56 | assert.deepEqual(api.directories, expectedMusic);
57 | });
58 |
59 | it("Should add directory", function () {
60 | api.addDirectory("/Pictures", "/home/user/pictures");
61 |
62 | assert.deepEqual(api.directories, expectedPictures);
63 | });
64 |
65 | it("Should add music directory", function () {
66 | api.addMusicDirectory("/Music", "/home/user/music");
67 |
68 | assert.deepEqual(api.directories, expectedMusic);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------