This is the place to share your import and playlist scripts. Try to not paste in code, rather upload complete scripts. Also provide a short description and a screenshot, showing how your script organizes the content.

For information on how to enable scripting, read the FAQ

File System Structured Import Script

If you want your videos displayed as they are structured in your file system, like in the following Example:




Modify your import.js as follows:


function addVideo(obj)
    var chain = new Array('Video', 'All Video');
    addCdsObject(obj, createContainerChain(chain));
    var last_path = getLastPath(obj.location);
    if (last_path)
        chain = new Array('Video', 'Directories', last_path);
        addCdsObject(obj, createContainerChain(chain));

With: (Note: in the code, replace “Video” with your appropriate root directory name)

function addVideo(obj)
    var chain, show, season;
    var location = obj.location.split('/');
    var rootindex = 0;
    for (var i = 0; i < location.length; i++)
        if (location[i] == "Video") //(Note: in the code, replace "Video" with your appropriate root directory name)
            rootindex = i;
    chain = new Array();
    if (rootindex + 1 < location.length - 1)
        for (i = rootindex + 1; i < location.length - 1; i++)
        addCdsObject(obj, createContainerChain(chain));

(Note: in the code, replace “Video” with your appropriate root directory name)

Download the full import.js script (this points to an older version, for now, update manually).

Yet another structured video/audio import script

The attached import script is fit to work on a specific filesystem structure, with a layout like this:

/data  /movies  /mymovie1
       /series  /myseries1  /season 1
                            /season 2
                /myseries2  /season 1
                            /season 2
                            /season 3

It parses the path to an avi, and checks the second piece ('series' or 'movies') to decide what to do. If you want it to work with longer/shorter/different paths, alter the script to use other numbers or array length and the actual names of the directories. It will create a layout in your player that is very similar to your filesystem:

Movies  >>  mymovie1  >>  mymovie1.avi
        >>  mymovie2  >>  mymovie2.avi

Series  >>  myseries1  >>  season 1  >>  myseries1-s1-ep1.avi
                       >>  season 2  >>  myseries1-s2-ep1.avi

… and so on.

Music is also heavily structured, albeit without depending on the filesystem, but strictly on the tags. The mp3 tags really really really really should have “artist”, “album”, “track”, “title” and “genre” set. Other tags are not being used.

The resulting layout in the player will be similar to this:

Music  >>  Artists  >>  my_artist1  >>  All Songs  >>  title1.mp3
                                                   >>  title2.mp3
                                                   >>  title3.mp3
                                                   >>  title4.mp3
                                    >>  Albums     >>  my_album1  >>  01 - title1.mp3
                                                                  >>  02 - title2.mp3
                                                   >>  my_album2  >>  01 - title3.mp3
                                                                  >>  02 - title4.mp3
       >>  Albums  >>  my_album1  >>  01 - title1.mp3
                                  >>  02 - title2.mp3
                   >>  my_album2  >>  01 - title3.mp3
                                  >>  02 - title4.mp3
       >>  Genres  >>  my_genre1  >>  my_artist1  >>  my_album1  >>  01 - title1.mp3

… etcetera. You get the point.

Download the full import-alver.js script.

simple structured import script

This one leaves out the first few dir names of the full path to the files.

With skip=4,

/home/something/download/mymovie/file.avi → Video/mymovie/file.avi

/home/something/download/mymovie/more/file2.avi → Video/mymovie/more/file2.avi

if the path to the movie is shorter, it will just put it in Video.

Skip is the number of '/' in the path to skip, or the number of directories + 1 to to skip.

change the addVideo function to:

function addVideo(obj)
    var dir = obj.location.split('/');
    var skip=4;
    var chain = new Array('Video');
    if(skip <= dir.length-2)
    addCdsObject(obj, createContainerChain(chain));

No Frills Filesystem Layout

This script is a fairly minimal diff on the 0.12.0 import script. Borrowing some ideas from the scripts here, this on has some attributes at the top which change the behavior. If left at the defaults it will do exactly what the 0.12.0 import script does. This can be somewhat helpful. Basically, when values are set, the Video and Pictures you import will be organized around your filesystem layout while the Music will be structured as default. There are options to set up top which can trim the layout in exchange for limiting the search field. Thus, if you have a bunch of pictures (ie DVD covers) that you don't want to show up in the photo browser, you can do that.

The uploader seems to be upset at this time, so here is a patch

--- import.js	2010-05-19 21:06:11.618617884 -0500
+++ import-jcpunk.js	2010-05-19 21:07:36.776111626 -0500
@@ -20,10 +20,36 @@
     $Id: import.js 2010 2009-01-11 19:10:43Z lww $
+// Set this to 'Yes' to have track numbers in front of the title
+// in album view
+var leadingTrackNumbers = 'No';
+// Set this to 'Yes' to have Images use full path information
+// note, if importImagesOnlyFrom is set, that path will be shortened
+var imagesUsePath = 'No';
+// Set this to 'Yes' to have Videos use full path information
+// note, if importVideosOnlyFrom is set, that path will be shortened
+var VideosUsePath = 'No';
+// Set any of these to prevent Mediatomb from indexing Audio/Video/Pictures
+// from anywhere else on the system, note this only takes one value
+var importAudioOnlyFrom = '';
+var importVideoOnlyFrom = '';
+var importImageOnlyFrom = '';
 function addAudio(obj)
+    if (importAudioOnlyFrom != '')
+    {
+	if (obj.location.substr(0,importAudioOnlyFrom.length) != importAudioOnlyFrom)
+	{
+	   print('Skipping: ' + obj.location + ' Not in ' + importAudioOnlyFrom);
+	   return;
+	}
+    }
     var desc = '';
     var artist_full;
@@ -89,24 +115,24 @@
         obj.meta[M_DESCRIPTION] = desc;
-// uncomment this if you want to have track numbers in front of the title
-// in album view
-    var track = obj.meta[M_TRACKNUMBER];
-    if (!track)
-        track = '';
+    if (leadingTrackNumbers.toLowerCase() == 'yes')
+    {
+	var track = obj.meta[M_TRACKNUMBER];
+    	if (!track)
+		track = '';
+    	else
+    	{
+        	if (track.length == 1)
+        	{
+            	track = '0' + track;
+        	}
+        	track = track + ' ';
+    	}
+    }
-        if (track.length == 1)
-        {
-            track = '0' + track;
-        }
-        track = track + ' ';
+	var track = '';
-    // comment the following line out if you uncomment the stuff above  :)
-    var track = '';
     var chain = new Array('Audio', 'All Audio');
     obj.title = title;
@@ -148,15 +174,37 @@
 function addVideo(obj)
+    if (importVideoOnlyFrom != '')
+    {
+        if (obj.location.substr(0,importVideoOnlyFrom.length) != importVideoOnlyFrom)
+        {
+           print('Skipping: ' + obj.location + ' Not in ' + importVideoOnlyFrom);
+           return;
+        }
+    }
     var chain = new Array('Video', 'All Video');
     addCdsObject(obj, createContainerChain(chain));
-    var last_path = getLastPath(obj.location);
-    if (last_path)
+    if (videosUsePath.toLowerCase() != 'yes')
-        chain = new Array('Video', 'Directories', last_path);
-        addCdsObject(obj, createContainerChain(chain));
+        var last_path = getLastPath(obj.location);
+        if (last_path)
+        {
+           chain = new Array('Video', 'Directories', last_path);
+           addCdsObject(obj, createContainerChain(chain));
+	}
+    }
+    else
+    {
+        var path = obj.location.replace(importVideoOnlyFrom,'');
+        var pathElements = new Array();
+        pathElements = path.split('/');
+        pathElements.pop();
+        pathElements.unshift('Video','Directories');
+        addCdsObject(obj, createContainerChain(pathElements));
 function addWeborama(obj)
@@ -171,6 +219,15 @@
 function addImage(obj)
+    if (importImageOnlyFrom != '')
+    {
+        if (obj.location.substr(0,importImageOnlyFrom.length) != importImageOnlyFrom)
+        {
+           print('Skipping: ' + obj.location + ' Not in ' + importImageOnlyFrom);
+           return;
+        }
+    }
     var chain = new Array('Photos', 'All Photos');
     addCdsObject(obj, createContainerChain(chain), UPNP_CLASS_CONTAINER);
@@ -181,11 +238,24 @@
         addCdsObject(obj, createContainerChain(chain), UPNP_CLASS_CONTAINER);
-    var last_path = getLastPath(obj.location);
-    if (last_path)
+    if (imagesUsePath.toLowerCase() != 'yes')
-        chain = new Array('Photos', 'Directories', last_path);
-        addCdsObject(obj, createContainerChain(chain));
+	var last_path = getLastPath(obj.location);
+    	if (last_path)
+    	{
+           chain = new Array('Photos', 'Directories', last_path);
+           addCdsObject(obj, createContainerChain(chain));
+        }
+    }
+    else
+    {
+	var path = obj.location.replace(importImageOnlyFrom,'');
+	var pathElements = new Array();
+	pathElements = path.split('/');
+	pathElements.pop();
+        pathElements.unshift('Photos','Directories');
+	addCdsObject(obj, createContainerChain(pathElements));

ABC Based Structured Import Script

It is possible to divide your content into subfolders based on the first letter of the title. After a while, you will end up with 26 folders. You can also group these first letters into less folders (less than 26 …). To do this in an easy way you can use the function called abcbox.

An example always works best, so let's say we want to add an album: Woodface by Crowded House. After it's imported we want to see the following structure:

        |  +-Crowded House
        |       +-Woodface


You see that it is put into the group ABCD. You can change this easily into ABC, or ABCDEF or anything else by changing a few lines in the code> Read on to make this work and to customize it to your needs:


1. Download the function abcbox and put into your script: common.js

2. Define the lettergroups

Let's say you want to have the next divisions: ABCD / EFGHI / JKLM / NOPQ / RSTUV / WXYZ. These are defined in an array: var boxwidth = new Array(4,5,4,4,5,4). You see that the number corresponds to the number of characters per box.

3. Call the function in the import script.

The function abcbox is defined as: function abcbox(stringtobox, boxtype, divchar).

  • stringtobox : title, artist or whatever you would like to add to an abcbox
  • boxtype : choice of a (predefined) set of abcboxes
  • divchar : DIViding CHARacter, put '-' here if you want to return -ABCD-, or '*' to end up with *ABCD*.

So when Crowded House should be put into -ABCD-, use: abcbox(artist, 2, '-').

You can also add an extra subdivision of first letters inside an abcbox. Just a matter of taste and number of media, I guess.

        |  +-A-
        |  +-B-
        |  +-C-
        |    +-Crowded House
        |       +-Woodface
        |  +-D-



Screenshot abcbox The image to the right shows a part of the structure of my Videos. It's based on the previous code from this section, but generated using the function abcboxes. Just to show that you can generate the same structure if you wish to.

Here you can download a working example of import.js and common.js. Note (2012-Aug-05). To get this to work correctly I had to edit import.js and change all instances of UPNP_CLASS_CONTAINER_MUSIC) to UPNP_CLASS_ITEM_MUSIC_TRACK).


  • Some groups are already defined, so you can choose from these
  • You are able to define your own, just change the values
  • To make a coherent structure, don't use too many different groups
  • Any number of letters inside a box is possible, as long as the total sum equals 26
  • Numbers and other characters are also accounted for

Previous Page

This piece is inspired by reading and using the original Import Scripting Pages, you can find them here.

Parsing Of File Names For TV Shows

This is how you can automagically organize your tv shows in folders that correspond to the name of the show and the season. For turing:

Videos\Heroes.S02E07.Out of Time.HDTV.XviD-LOL.[TvT].avi


Videos\TV Shows\My Name Is Earl\Season 02\Episode 19 - Harassed A Reporter
Videos\TV Shows\Heroes\Season 02\Episode 07 - Out Of Time
Videos\TV Shows\House\Season 03\Episode 20

It looks like this:

Note: Do make a backup of your import script if something went wrong!

        TV Shows
                My Show That I Like
                        Season 01
                                Episode 01 - Things That Matters
                                Episode 02
                                Episode 03 - Another Title

Add the following code in the top of the function addVideo in your import script Replace:


If you are using the original script, the result should look like this:

function addVideo(obj)
    var chain = new Array('Video');
    addCdsObject(obj, createContainerChain(chain));

Now it is time to add the function checkIfSerieAndAdd and the required functions.

Note: If you have a problem with the following code, see the following forum post (original author may want to recheck the code posted here, as I am not very familiar with javascript regexes).

Note again: I had to remove the javascript coloring because it was tampering with the patterns. This script should now work correctly. In the forum post there are some additional patterns if these does not work for all of your shows.

Add the following code to the end of the file:

function checkIfSerieAndAdd(obj)
  var excludeSamples = true;
  var fixEpisodeNames = true;
  var patterns = [  /(.*)s(\d\d)e(\d\d)(\D.*)/i,
					/(\D*)(\d)[^0-9](\d\d)(\D.*)/i  ];
  for(var i=0;i<patterns.length;i++){
	  var match = patterns[i].exec(obj.title);
		var name = fixSerieName(match[1]);
		if(name.length == 0)
		var season = parseFloat(match[2]);
		if(season == 0)
		var episode = parseFloat(match[3]);
		var leftover = match[4];
		var r = new RegExp();
		if(excludeSamples && /\Wsample\W/i.test(leftover))
			return true;		
		if(episode == 0){
			// Some malformed string
			leftover = obj.title;			
		var chain = new Array('Video', 'TV Shows');
		var paddedSeason = 'Season ' + padLeft(season, '0', 2);

		var paddedEpisode = padLeft(episode, '0', 2);
		if(paddedEpisode == 'NaN')
			paddedEpisode = '';
			paddedEpisode = ' ' + paddedEpisode;
			var commonabbr = [ 'divx', 'xvid', 'dvdrip', 'hdtv', 'lol', 'axxo', 'repack', 'xor', 
								'pdtv', 'real', 'vtv', 'caph', '2hd', 'proper', 'fqm', 'uncut', 
								'topaz', 'tvt', 'notv', 'fpn', 'fov', 'orenji', '0tv', 'omicron', 
								'dsr', 'ws', 'sys', 'crimson', 'wat', 'hiqt', 'internal']
			for(var j=0;j<commonabbr.length;j++){
				var re = new RegExp("[\\W|_]" + commonabbr[j] + "[\\W|_]", 'gi');
				leftover = leftover.replace(re, '.');
				leftover = leftover.replace(/\.\./g, '.');
			leftover = leftover.replace(/\.\w*$/, ' ');
			leftover = fixSerieName(leftover);
			if(leftover.length > 0)
				leftover = ' - ' + leftover;
		obj.title = 'Episode' + paddedEpisode + leftover;

		addCdsObject(obj, createContainerChain(chain));
		return true;

  return false;

function fixSerieName(name){
	name = name.replace(/_/g, ' ');
	name = name.replace(/\./g, ' ');
	name = name.replace(/  /g, ' ');
	name = removeStartingDashesAndSpaces(name);
	name = removeEndingDashesAndSpaces(name);
	return toProperCase(name);
function removeStartingDashesAndSpaces(name){
	if(name.length == 0)
		return name;
	while(name.indexOf(' ') == 0 || name.indexOf('-') == 0){
		name = name.replace(/^ /g, '');
		name = name.replace(/^-/g, '');
	return name;
function removeEndingDashesAndSpaces(name){
	if(name.length == 0)
		return name;
	while(name.lastIndexOf(' ') == name.length - 1 || name.lastIndexOf('-') == name.length - 1){
		name = name.replace(/ $/g, '');
		name = name.replace(/-$/g, '');
	return name;
function toProperCase(s)
  return s.toLowerCase().replace(/^(.)|\s(.)/g, 
          function($1) { return $1.toUpperCase(); });

function padLeft(str, pad, count) {
	str += '';
	while(str.length < count){
		str = pad + str;
	return str;

Save your modified import.js and re-import all your videos.

Importing Tracks From flac+cue CD Image

This allows you to import CD images in flac format with an external cue sheet to your media server. The script assumes that the whole album is contained in one flac encoded file with an accompanying text format cue file describing the track layout in within the file and the meta data for the whole disc and individual tracks. The tracks are imported as “dummy” urls containing only the meta data, the flac file name, and the track location within the file. The urls are then provided through a transcoding script which produces the audio in wav format for the clients.

Changes to config.xml defining some mappings

* <import><mappings><extension-mimetype> add: <map from=“cue” to=“audio/x-cue”/>

* <import><mappings><mimetype-contenttype> add: <treat mimetype=“audio/x-cue” as=“playlist”/>

* <transcoding><mimetype-profile-mappings> add: <transcode mimetype=“audio/x-cue+flac” using=“flac2raw”/>

* <transcoding><profiles> add:

<profile name="flac2raw" enabled="yes" type="external">
  <agent command="" arguments="%in %out"/>
  <buffer size="1048576" chunk-size="131072" fill-size="262144"/>

Changes to import.js and common.js

To be able to call addAudio() function from playlist.js, it must be transferred from import.js to common.js. Also, the .cue extension must be specified to be a known playlist extension by changing getPlaylistType() function in common.js to be

function getPlaylistType(mimetype)
    if (mimetype == 'audio/x-mpegurl')
        return 'm3u';
    if (mimetype == 'audio/x-scpls')
        return 'pls';
    if (mimetype == 'audio/x-cue')
        return 'cue';
    return '';

Cue sheet parsing

The majority of the required scripting is done in playlist.js. The cue sheet parser is added to the main function as an own branch in addition to .m3u and .pls handling. After the parser has collected enough information of a track, it calls a new createSubItem() function which creates an object with the correct meta data, assigns type OBJECT_TYPE_ITEM_EXTERNAL_URL to it, and encodes the track location in the url in the form


This tells that the track location in disc is the range 1:23.45-3:45.67 in the given file. After all metadata is assigned to the newly created track object, it is added to the data base with addAudio() function (remember, the one transferred from import.js to common.js).

The full playlists.js script.

The transcoding script decoding the url for an external program is given in the transcoding section.

Categorize Classical Music and Other Customizations

This import-badpenguin.js implements the following features:

Maintains top level music chains for Classical, Holiday and Country Music. All other music goes into standard Audio top level chain. Classical and Holiday music get parsed for orchestras, choirs, and composers. Each gets a By Orchestra, By Choir, By Composer etc.. section.


Categories of shared items will not cross, for example photos found during the import of music will not be populated into the Pictures chain.

Rock gets 4 new entries under the Genres chain - 50's Rock, 60's Rock, 70's Rock, and 80's Rock.


Images are added in chains that mirror the filesystem directory structure. With the exception that the top level of each 1-off image category gets an “All XXXX Photos” category. For example /pics/pets/dogs/jessi.jpg will get an entry in /pics/pets/All Pets and /pics/pets/dogs.


If video imports are in /video/Series, it will create chains for /video/Series/Seaason. If the video is not in series, it will import into chains that mirror the filesystem directory structure.



Edit the first few lines of the import script, the following items need to be changed to your own directory locations:

var homeAudio = '/storage03/media/music';
var homeVideo = '/storage03/media/video';
var homeImage = '/storage03/media/pictures';

Configure the script as your import script (config.xml) and set virtual layout to js:

      <virtual-layout type="js">

For composers to be parsed, add the following to config.xml:

        <add-data tag="TCOM">

Unfortunately, the composer tag has wildly different entries for similar composers. To clean up categorizing by composer a bit, this script parses common composer names from either the composer field or title of the song. To customize the composers that are parsed, edit the parseComposer() function down towards the bottom of the import script.

	new Array(/(.*)copland(.*)/i,'Copland, Aaron'),
	new Array(/(.*)debussy(.*)/i,'Debussy, Claude'),
	new Array(/(.*)grieg(.*)/i,'Grieg, Edvard'),
	new Array(/(.*)gruber(.*)/i,'Gruber, Franz Xaver'),

Importing EXIF Data for Raw (cr2, nef) Photos

In order to view raw images on Mediatomb, first implement transcoding from cr2 to jpeg, as documented in the transcoding section of this wiki.

This will enable you to view your images, but no dates or other EXIF data will be picked up by the import script. It is required to use libextractor in order to import EXIF data.

libxtractor uses libexiv2 to retrieve exif data. This library is capable of parsing raw image files. However, the most recent (as of April 2009) released version of libextractor includes a copy of an ancient version of libexiv2 that does not support raw images very well. For my Canon (cr2) images I get the following results:

  • The date and other data are returned as multiple instances of the “software” keyword
  • Some of my cr2 files cause libextractor to segfault.

Christian Grothoff (one of the libextractor authors) was kind enough to change libextractor to use the latest libexiv2 found on the build machine. This fixes the above problems, and enables me to retreive most of the EXIF data from my raw files.

This means that until the versions of libextractor included with various distros incorporate Christian's fixes, you need to download and build libextractor from SVN source. Note: there are tar archives of libextractor available here, however the latest (as of April 2009) tar archive, libextractor-0.5.22.tar.gz, doesn't include the fix.

The following build instructions are for Ubuntu 8.04 (Hardy) but should be easily adaptable to other distros.

To build libextractor from SVN, libtool version 2.2.0 or later is required. Use the command libtool —-version to check your version. Hardy includes libtool 1.5.26, so we need to upgrade it. I used Prevu to build libtool version 2.2.4 from Intrepid.

  • If you don't have Prevu installed, install and configure it using the directions in the Ubuntu wiki. Don't forget to run prevu-init.
  • To build libtool 2.2.4 use the commmand
  • Use synaptic to install the packages. Start synaptic and click the 'Reload' button. Then install the packages libtool, libtool-doc, libltdl7, and libltdl7-dev from Local/main.
  • If your distro doesn't have Prevu, you can download libtool source from here, then configure, make, and install it.

Next you need to install libexiv2. The Hardy repository contains an old version (0.16) that can be installed with the command sudo apt-get install exiv2 libexiv2-dev. I found this package worked OK, but dumped lots of warnings when run on by cr2 files. So I used Prevu to get the latest (0.18-1) from the Jaunty repository using the command


Start synaptic and 'Reload'. Install packages libexiv2-5, libexiv2-dev, and libexiv2-doc from Local/main, and package exiv2 from Local/universe.

Now, you are ready to download and build libextractor. Run the following commands:

sudo apt-get remove extract libextractor-dev libextractor-plugins libextractor1c2a (if already installed)
svn checkout
cd Extractor
./configure --prefix=/usr --enable-exiv2
sudo make install

Test your build by running the extract command on one of your raw files. You should see something like this:

$ extract IMG_0986.CR2 
white balance - Auto
size - 4272x2848
image quality - RAW
macro mode - Off
metering mode - Multi-segment
exposure mode - Aperture priority
iso speed - 400
focal length - 41.0 mm
flash bias - 0 EV
flash - No, compulsory
exposure bias - 0
aperture - F16
exposure - 1/160 s
date - 2009:01:23 13:14:20
orientation - right, top
camera model - Canon EOS DIGITAL REBEL XSi
camera make - Canon
creation date - 2009:01:23 13:14:20
source - Canon EOS DIGITAL REBEL XSi
size - 2256x1504
mimetype - image/tiff

If you see software keywords, or get a segfault, then you don't have the latest libextractor installed correctly.

Next you need to enable libextractor in MediaTomb:

  • If your Mediatomb build does not include libextractor support, rebuild Mediatomb including the ./configure —-enable-libextractor option.
  • The fix to libextractor only changes the dynamically loaded shared object libraries libextractor_tiff and libextractor_exiv2. This means you don't have to rebuild MediaTomb if you currently have libextractor support compiled in.
  • The following libextractor keywords are supported.

As an example, lets import the date:

  • Add the following to the '<import>' section of your config.xml:
          <add-data tag="EXTRACTOR_DATE"/>
  • In import.js, replace the date computation in function addImage with the following. Note that the date on my cr2 files is different from the JPEG date format expected by MediaTomb, so I needed to translate the date. Different code may be necessary of other camera vendors.
    var date = obj.meta[M_DATE]; 
    if (date) 
        chain = new Array('Photos', 'Date', date); 
        addCdsObject(obj, createContainerChain(chain), UPNP_CLASS_CONTAINER); 
        date = obj.aux["EXTRACTOR_DATE"]; 
        if (date) 
            //for CR2 files, libextractor returns YYYY:MM:DD HH:MM:SS 
            //first, chop off the time 
            if (date.length > 10) date = date.substr(0, 10); 
            //next, change ':' to '-' in order to be consistent with JPEG date format
            date = date.replace(/:/g, '-'); 
            chain = new Array('Photos', 'Date', date); 
            addCdsObject(obj, createContainerChain(chain), UPNP_CLASS_CONTAINER);  
  • Finally, blow away your database, re-import your files, and you should be good to go!
  • When importing my cr2 files, a few of them give this error:
    Error: Directory Unknown with nnnnn entries considered invalid; not read.
    This error does not prevent the file from being imported and can be safely ignored.
scripting/scripting.txt · Last modified: 2012/08/05 12:47 by gaddman