├── .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 | ![upnpserver icon](icon/icon_128.png) 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 | ![upnpserver icon](icon_128.png) 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) + ''; 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 + ""; 191 | } else if (trimVal.length) { 192 | s += ">" + trimVal + ""; 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 | --------------------------------------------------------------------------------