var autorec_caption = params[12].value;
var timerec_caption = params[13].value;
var image = params[14].value;
+ var copyright_year = params[15].value;
+ var credits = params[16].value;
+ var keyword = params[17].value;
+ var category = params[18].value;
+ var first_aired = params[19].value;
+ var genre = params[20].value;
var content = '';
var but;
if (duplicate)
content += '<div class="x-epg-meta"><font color="red"><span class="x-epg-prefix">' + _('Will be skipped') + '<br>' + _('because it is a rerun of:') + '</span>' + tvheadend.niceDate(duplicate * 1000) + '</font></div>';
+ var icons = tvheadend.getContentTypeIcons({"category" : category, "genre" : genre});
+ if (icons)
+ content += '<div class="x-epg-icons">' + icons + '</div>';
+ var displayTitle = title;
+ if (copyright_year)
+ displayTitle += " (" + copyright_year + ")";
if (title)
- content += '<div class="x-epg-title">' + title + '</div>';
+ content += '<div class="x-epg-title">' + displayTitle + '</div>';
if (subtitle)
content += '<div class="x-epg-title">' + subtitle + '</div>';
if (episode)
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('Scheduled Start Time') + ':</span><span class="x-epg-body">' + tvheadend.niceDate(start_real * 1000) + '</span></div>';
if (stop_real)
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('Scheduled Stop Time') + ':</span><span class="x-epg-body">' + tvheadend.niceDate(stop_real * 1000) + '</span></div>';
+ /* We have to *1000 here (and not in epg.js) since Date requires ms and epgStore has it already converted */
+ if (first_aired)
+ content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('First Aired') + ':</span><span class="x-epg-body">' + tvheadend.niceDateYearMonth(first_aired * 1000, start_real * 1000) + '</span></div>';
if (duration)
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('Duration') + ':</span><span class="x-epg-body">' + parseInt(duration / 60) + ' ' + _('min') + '</span></div>';
if (chicon) {
content += '<div class="x-epg-desc">' + desc + '</div>';
content += '<hr class="x-epg-hr"/>';
}
+ content += tvheadend.getDisplayCredits(credits);
+ if (keyword)
+ content += tvheadend.sortAndAddArray(keyword, _('Keywords'));
+ if (category)
+ content += tvheadend.sortAndAddArray(category, _('Categories'));
if (status)
content += '<div class="x-epg-meta"><span class="x-epg-prefix">' + _('Status') + ':</span><span class="x-epg-body">' + status + '</span></div>';
if (filesize)
uuid: uuid,
list: 'channel_icon,disp_title,disp_subtitle,episode,start_real,stop_real,' +
'duration,disp_description,status,filesize,comment,duplicate,' +
- 'autorec_caption,timerec_caption,image'
+ 'autorec_caption,timerec_caption,image,copyright_year,credits,keyword,category,' +
+ 'first_aired,genre',
},
success: function(d) {
d = json_decode(d);
}
}
+
+tvheadend.displayDuplicate = function(value, meta, record) {
+ if (value == null)
+ return '';
+ var is_dup = record.data['duplicate'];
+ if (is_dup)
+ return "<span class='x-epg-duplicate'>" + value + "</span>";
+ else
+ return value;
+}
+
/** Render an entry differently if it is a duplicate */
tvheadend.displayWithDuplicateRenderer = function(value, meta, record) {
return function() {
return function(value, meta, record) {
- if (value == null)
- return '';
- var is_dup = record.data['duplicate'];
- if (is_dup)
- return "<span class='x-epg-duplicate'>" + value + "</span>";
- else
- return value;
+ return tvheadend.displayDuplicate(value, meta, record);
}
}
}
+tvheadend.displayWithYearAndDuplicateRenderer = function(value, meta, record) {
+ return function() {
+ return function(value, meta, record) {
+ value = tvheadend.getDisplayTitle(value, record);
+ return tvheadend.displayDuplicate(value, meta, record);
+ }
+ }
+}
+
+tvheadend.displayWithYearRenderer = function(value, meta, record) {
+ return function() {
+ return function(value, meta, record) {
+ value = tvheadend.getDisplayTitle(value, record);
+ return value;
+ }
+ }
+}
+
/**
*
*/
}
},
del: true,
- list: 'enabled,duplicate,disp_title,disp_subtitle,episode,channel,' +
+ list: 'category,enabled,duplicate,disp_title,disp_subtitle,episode,channel,' +
'image,' +
+ 'copyright_year,' +
'start_real,stop_real,duration,pri,filesize,' +
- 'sched_status,errors,data_errors,config_name,owner,creator,comment',
+ 'sched_status,errors,data_errors,config_name,owner,creator,comment,genre',
columns: {
disp_title: {
- renderer: tvheadend.displayWithDuplicateRenderer()
+ renderer: tvheadend.displayWithYearAndDuplicateRenderer()
},
disp_subtitle: {
renderer: tvheadend.displayWithDuplicateRenderer()
},
filesize: {
renderer: tvheadend.filesizeRenderer()
+ },
+ genre : {
+ renderer: function(vals, meta, record) {
+ return function(vals, meta, record) {
+ var r = [];
+ Ext.each(vals, function(v) {
+ v = tvheadend.contentGroupFullLookupName(v);
+ if (v)
+ r.push(v);
+ });
+ if (r.length < 1) return "";
+ return r.join(',');
+ }
+ }
}
},
sort: {
direction: 'ASC'
},
plugins: [actions],
- lcol: [actions],
+ lcol: [
+ actions,
+ tvheadend.contentTypeAction,
+ ],
tbar: [stopButton, abortButton],
selected: selected,
beforeedit: beforeedit
del: false,
list: 'disp_title,disp_subtitle,episode,channelname,' +
'start_real,stop_real,duration,filesize,' +
- 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment',
+ 'copyright_year,' +
+ 'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment,',
columns: {
+ disp_title: {
+ renderer: tvheadend.displayWithYearRenderer(),
+ },
filesize: {
renderer: tvheadend.filesizeRenderer()
}
delquestion: _('Do you really want to delete the selected recordings?') + '<br/><br/>' +
_('The associated file will be removed from storage.'),
list: 'disp_title,disp_subtitle,episode,channelname,' +
+ 'image,' +
+ 'copyright_year,' +
'start_real,stop_real,duration,filesize,status,' +
'sched_status,errors,data_errors,playcount,url,config_name,owner,creator,comment',
columns: {
+ disp_title: {
+ renderer: tvheadend.displayWithYearRenderer(),
+ },
filesize: {
renderer: tvheadend.filesizeRenderer()
}
edit: { params: { list: tvheadend.admin ? "retention,owner,comment" : "retention,comment" } },
del: true,
list: 'disp_title,disp_subtitle,episode,channelname,' +
+ 'image,' +
+ 'copyright_year,' +
'start_real,stop_real,duration,status,' +
'sched_status,errors,data_errors,url,config_name,owner,creator,comment',
+ columns: {
+ disp_title: {
+ renderer: tvheadend.displayWithYearRenderer(),
+ },
+ },
sort: {
field: 'start_real',
direction: 'ASC'
if (chicon)
content += '<div class="x-epg-left">';
+ var icons = tvheadend.getContentTypeIcons(event);
+ if (icons)
+ content += '<div class="x-epg-icons">' + icons + '</div>';
content += '<div class="x-epg-title">' + event.title;
if (event.subtitle)
content += " : " + event.subtitle;
- if (event.copyrightYear)
- content += " (" + event.copyrightYear + ")";
+ if (event.copyright_year)
+ content += " (" + event.copyright_year + ")";
content += '</div>';
if (event.episodeOnscreen)
content += '<div class="x-epg-title">' + event.episodeOnscreen + '</div>';
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('Start Time') + ':</span><span class="x-epg-body">' + tvheadend.niceDate(event.start) + '</span></div>';
if (event.stop)
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('End Time') + ':</span><span class="x-epg-body">' + tvheadend.niceDate(event.stop) + '</span></div>';
+ if (event.first_aired)
+ content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('First Aired') + ':</span><span class="x-epg-body">' + tvheadend.niceDateYearMonth(event.first_aired, event.start) + '</span></div>';
if (duration)
content += '<div class="x-epg-time"><span class="x-epg-prefix">' + _('Duration') + ':</span><span class="x-epg-body">' + parseInt(duration / 60) + ' ' + _('min') + '</span></div>';
if (chicon) {
content += '<div class="x-epg-desc">' + event.description + '</div>';
if (event.summary || event.description)
content += '<hr class="x-epg-hr"/>';
-
- // Helper function for common code to sort an array, convert to CSV and
- // return the string to add to the content.
- function sortAndAddArray(arr, title) {
- arr.sort();
- var csv = arr.join(", ");
- if (csv)
- return '<div class="x-epg-meta"><span class="x-epg-prefix">' + title + ':</span><span class="x-epg-body">' + csv + '</span></div>';
- else
- return '';
- }
-
- if (event.credits) {
- // Our cast (credits) map contains details of actors, writers,
- // etc. so split in to separate categories for displaying.
- var castArr = [];
- var crewArr = [];
- var directorArr = [];
- var writerArr = [];
- var cast = ["actor", "guest", "presenter"];
- // We use arrays here in case more tags in the future map on to
- // director/writer, e.g., SchedulesDirect breaks it down in to
- // writer, writer (adaptation) writer (screenplay), etc. but
- // currently we just have them all as writer.
- var director = ["director"];
- var writer = ["writer"];
-
- for (key in event.credits) {
- var type = event.credits[key];
- if (cast.indexOf(type) != -1)
- castArr.push(key);
- else if (director.indexOf(type) != -1)
- directorArr.push(key);
- else if (writer.indexOf(type) != -1)
- writerArr.push(key);
- else
- crewArr.push(key);
- };
-
- content += sortAndAddArray(castArr, _('Starring'));
- content += sortAndAddArray(directorArr, _('Director'));
- content += sortAndAddArray(writerArr, _('Writer'));
- content += sortAndAddArray(crewArr, _('Crew'));
- }
+ content += tvheadend.getDisplayCredits(event.credits);
if (event.keyword)
- content += sortAndAddArray(event.keyword, _('Keywords'));
+ content += tvheadend.sortAndAddArray(event.keyword, _('Keywords'));
if (event.category)
- content += sortAndAddArray(event.category, _('Categories'));
+ content += tvheadend.sortAndAddArray(event.category, _('Categories'));
if (event.starRating)
- content += '<div class="x-epg-meta"><span class="x-epg-prefix">' + _('Star Rating') + ':</span><span class="x-epg-body">' + event.starRating + '</span></div>';
+ content += '<div class="x-epg-meta"><span class="x-epg-prefix">' + _('Star Rating') + ':</span><span class="x-epg-desc">' + event.starRating + '</span></div>';
if (event.ageRating)
- content += '<div class="x-epg-meta"><span class="x-epg-prefix">' + _('Age Rating') + ':</span><span class="x-epg-body">' + event.ageRating + '</span></div>';
+ content += '<div class="x-epg-meta"><span class="x-epg-prefix">' + _('Age Rating') + ':</span><span class="x-epg-desc">' + event.ageRating + '</span></div>';
if (event.genre) {
var genre = [];
Ext.each(event.genre, function(g) {
type: 'date',
dateFormat: 'U' /* unix time */
},
+ {
+ name: 'first_aired',
+ type: 'date',
+ dateFormat: 'U' /* unix time */
+ },
{ name: 'starRating' },
{ name: 'credits' },
{ name: 'category' },
{ name: 'keyword' },
{ name: 'ageRating' },
- { name: 'copyrightYear' },
+ { name: 'copyright_year' },
+ { name: 'new' },
{ name: 'genre' },
{ name: 'dvrUuid' },
{ name: 'dvrState' },
return "";
}
}),
+ tvheadend.contentTypeAction,
{
width: 250,
id: 'title',
var clickable = tvheadend.regexEscape(record.data['title']) !=
epgStore.baseParams.title;
setMetaAttr(meta, record, value && clickable);
+ value = tvheadend.getDisplayTitle(value, record);
return !value ? '' : (clickable ? lookup : '') + value;
},
listeners: { click: { fn: clicked } }
return r;
}
+// We have "major" and "minor" mappings since we want things like
+// "Movie" to be preferred to minor elements such as "Comedy" so we
+// always end up displaying "Movie-Comedy" rather than having "Movie"
+// sometimes hidden in middle of other icons.
+//
+// Although we can insert the characters here, we use the hex
+// values because editors don't always work well with these
+// characters.
+//
+// The comments refer to the official unicode name
+//
+// These categories should _not_ be subject to internationalization
+// since many non-English xmltv providers appear to supply English
+// words for categories, presumably for compatibility with
+// mapping to a genre.
+var catmap_major = {
+ "movie" : "🎞", // Film frames
+ "news" : "📰", // Newspaper
+ "series" : "📺", // Television
+ "sports" : "🏅", // Sports medal
+};
+
+var catmap_minor = {
+ // These are taken from the frequent categories in SD and then
+ // sorted by name. They display reasonably well on a modern
+ // font.
+ "action" : "🏹", // Bow and Arrow
+ "adults only" : "🔞", // No one under eighteen symbol
+ "adventure" : "🏹", // Bow and Arrow
+ "animals" : "🐾", // Paw prints
+ "animated" : "✏️", // Pencil
+ "art" : "🎨", // Artist pallette
+ "auction" : "💸", // Money with wings
+ "auto racing" : "🏎", // Racing car
+ "auto" : "🏎", // Racing car
+ "baseball" : "⚾", // Baseball
+ "basketball" : "f3c0;", // Basketball and hoop
+ "boxing" : "🥊", // Boxing glove
+ "bus./financial" : "📈", // Chart with upwards trend
+ "children" : "👶", // Baby
+ "comedy" : "😀", // Grinning face
+ "computers" : "💻", // Personal computer
+ "community" : "👪", // Family
+ "cooking" : "🔪", // Cooking knife
+ "crime drama" : "👮", // Police officer
+ "dance" : "💃", // Dancer
+ "educational" : "🎓", // Graduation cap
+ "fantasy" : "🦄", // Unicorn face
+ "fashion" : "👠", // High heeled shoe
+ "figure skating" : "⛸", // Ice skate
+ "fishing" : "🎣", // Fishing pole and fish
+ "football" : "🏈", // American Football (not soccer)
+ "game show" : "🎲", // Game die
+ "gymnastics" : "🤸", // Person doing cartwheel
+ "history" : "🏰", // Castle
+ "holiday" : "🛫", // Airplane departure
+ "horror" : "💀", // Skull
+ "horse" : "🐴", // Horse face
+ "house/garden" : "🏡", // House with garden
+ "interview" : "💬", // Speech balloon
+ "law" : "👮", // Police officer
+ "martial arts" : "🥋", // Martial arts uniform
+ "medical" : "🚑", // Ambulance
+ "military" : "🎖", // Military medal
+ "miniseries" : "🔗", // Link symbol
+ "motorcycle" : "🏍", // Racing motorcycle
+ "music" : "🎵", // Musical note
+ "musical" : "🎵", // Musical note
+ "mystery" : "🔍", // Left pointing magnifying glass
+ "nature" : "🐘", // Elephant
+ "paranormal" : "👻", // Ghost
+ "poker" : "🂱", // Playing card ace of hearts
+ "politics" : "🗳", // Ballot box with ballot
+ "pro wrestling" : "🤼", // Wrestlers
+ "reality" : "📸", // Camera with flash
+ "religious" : "🛐", // Place of worship
+ "romance" : "❤️", // Red Heart
+ "romantic comedy" : "❤️", // Red Heart
+ "science fiction" : "👽", // Extraterrestrial alien
+ "science" : "🔬", // Microscope
+ "shopping" : "🛍", // Shopping bags
+ "sitcom": "😀", // Grinning face
+ "skiing" : "⛷", // Skier
+ "soap" : "🝔", // Alchemical symbol for soap
+ "soccer" : "⚽", // Soccer ball
+ "sports talk" : "💬", // Speech balloon
+ "spy": "🕵", // Spy
+ "standup" : "🎤", // Microphone
+ "swimming" : "🏊", // Swimmer
+ "talk" : "💬", // Speech balloon
+ "technology" : "💻", // Personal computer
+ "tennis" : "🎾", // Tennis racquet and ball
+ "theater" : "f3ad;", // Performing arts
+ "travel" : "🛫", // Airplane departure
+ "war" : "🎖", // Military medal
+ "weather" : "⛅", // Sun behind cloud
+ "weightlifting" : "🏋", // Person lifting weights
+ "western" : "🌵", // Cactus
+};
+
+// These are mappings for OTA genres
+var genre_major = {
+ // And genre major-numbers in hex
+ "10" : "📺", // Television: can't distinguish movie / tv
+ "20" : "📰", // Newspaper
+ "30" : "🎲", // Game die
+ "40" : "🏅", // Sports medal
+ "50" : "👶", // Baby
+ "60" : "🎵", // Musical note
+ "70" : "🎭", // Performing arts
+ "80" : "🗳", // Ballot box with ballot
+ "90" : "🎓", // Graduation cap
+ "a0" : "⛺", // Tent
+};
+
+var genre_minor = {
+ "11" : "🕵", // Spy
+ "12" : "🏹", // Bow and Arrow
+ "13" : "👽", // Extraterrestrial alien
+ "14" : "😀", // Grinning face
+ "15" : "🝔", // Alchemical symbol for soap
+ "16" : "❤️", // Red Heart
+ "18" : "🔞", // No one under eighteen symbol
+ "33" : "💬", // Speech balloon
+ "43" : "⚽", // Soccer ball
+ "44" : "🎾", // Tennis racquet and ball
+ "73" : "🛐", // Place of worship
+ "91" : "🐘", // Elephant
+ "a1" : "🛫", // Airplane departure
+ "a5" : "🔪", // Cooking knife
+ "a6" : "🛒", // Shopping trolley
+ "a7" : "🏡", // House with garden
+};
+
+tvheadend.uniqueArray = function(arr) {
+ var unique = [];
+ for ( var i = 0 ; i < arr.length ; ++i ) {
+ if ( unique.indexOf(arr[i]) == -1 )
+ unique.push(arr[i]);
+ }
+ return unique;
+}
+
+
+tvheadend.getContentTypeIcons = function(rec) {
+ var ret_major = [];
+ var ret_minor = [];
+ var cat = rec.category
+ if (cat && cat.length) {
+ cat.sort();
+ for ( var i = 0 ; i < cat.length ; ++i ) {
+ var v = cat[i];
+ v = v.toLowerCase();
+ var l = catmap_major[v];
+ if (l) ret_major.push(l);
+ l = catmap_minor[v];
+ if (l) ret_minor.push(l)
+ }
+ } else {
+ // Genre code
+ var gen = rec.genre;
+ if (gen) {
+ for (var i = 0; i < gen.length; ++i) {
+ var genre = parseInt(gen[i]);
+ if (genre) {
+ // Convert number to hex to make lookup easier to
+ // cross-reference with epg.c
+ var l = genre_major[(genre & 0xf0).toString(16)];
+ if (l) ret_major.push(l);
+ l = genre_minor[genre.toString(16)];
+ if (l) ret_minor.push(l)
+ }
+ }
+ }
+ }
+
+ var ret = "";
+ if (rec.new)
+ ret += "🆕"; // Squared New
+ return ret + tvheadend.uniqueArray(ret_major).join("") + tvheadend.uniqueArray(ret_minor).join("");
+}
+
+tvheadend.displayCategoryIcon = function(value, meta, record, ri, ci, store) {
+ if (value == null)
+ return '';
+ var icons = tvheadend.getContentTypeIcons(record.data);
+ if (icons.length < 1) return '';
+ return icons;
+}
+
+tvheadend.contentTypeAction = {
+ width: 75,
+ id: 'category',
+ header: _("Content Type"),
+ tooltip: _("Content Type"),
+ dataIndex: 'category',
+ renderer: tvheadend.displayCategoryIcon,
+};
+
+tvheadend.getDisplayTitle = function(title, record) {
+ if (!title) return title;
+ var year = record.data['copyright_year'];
+ if (year)
+ title += " (" + year + ")";
+ return title;
+}
+
+// Helper function for common code to sort an array, convert to CSV and
+// return the string to add to the content.
+tvheadend.sortAndAddArray = function (arr, title) {
+ arr.sort();
+ var csv = arr.join(", ");
+ if (csv)
+ return '<div class="x-epg-meta"><span class="x-epg-prefix">' + title + ':</span><span class="x-epg-desc">' + csv + '</span></div>';
+ else
+ return '';
+}
+
+tvheadend.getDisplayCredits = function(credits) {
+ if (!credits)
+ return "";
+ if (credits instanceof Array)
+ return "";
+
+ var content = "";
+ // Our cast (credits) map contains details of actors, writers,
+ // etc. so split in to separate categories for displaying.
+ var castArr = [];
+ var crewArr = [];
+ var directorArr = [];
+ var writerArr = [];
+ var cast = ["actor", "guest", "presenter"];
+ // We use arrays here in case more tags in the future map on to
+ // director/writer, e.g., SchedulesDirect breaks it down in to
+ // writer, writer (adaptation) writer (screenplay), etc. but
+ // currently we just have them all as writer.
+ var director = ["director"];
+ var writer = ["writer"];
+
+ for (var key in credits) {
+ var type = credits[key];
+ if (cast.indexOf(type) != -1)
+ castArr.push(key);
+ else if (director.indexOf(type) != -1)
+ directorArr.push(key);
+ else if (writer.indexOf(type) != -1)
+ writerArr.push(key);
+ else
+ crewArr.push(key);
+ };
+
+ content += tvheadend.sortAndAddArray(castArr, _('Starring'));
+ content += tvheadend.sortAndAddArray(directorArr, _('Director'));
+ content += tvheadend.sortAndAddArray(writerArr, _('Writer'));
+ content += tvheadend.sortAndAddArray(crewArr, _('Crew'));
+ return content;
+}
+
/**
* Change uilevel
*/
'<div class="x-nice-time">' + d.toLocaleTimeString() + '</div>';
}
+/* Date format when time is not needed, e.g., first_aired time is
+ * often 00:00. Also takes a reference date so if the dt can be made
+ * nicer such as "Previous day" then we will use that instead.
+ */
+tvheadend.niceDateYearMonth = function(dt, refdate) {
+ var d = new Date(dt);
+ // If we have a reference date then we try and make the
+ // date nicer.
+ if (refdate) {
+ var rd = new Date(refdate);
+ if (rd.getYear() == d.getYear() &&
+ rd.getMonth() == d.getMonth() &&
+ rd.getDate() == d.getDate()) {
+ var when;
+ if (rd.getHours() == d.getHours() &&
+ rd.getMinutes() == d.getMinutes()) {
+ when = _("Premiere");
+ } else {
+ when = _("Same day");
+ }
+ return '<div class="x-nice-dayofweek">' + when + '</div>';
+ } else {
+ // Determine if it is previous day. We can't just subtract
+ // timestamps since a programme on at 8pm could have
+ // a previous shown timestamp of 00:00 on previous day,
+ // so would be > 86400 seconds ago. So, create temporary
+ // dates with timestamps of 00:00 and compare those.
+ var d0 = new Date(d);
+ var rd0 = new Date(rd);
+ d0.setHours(0);
+ d0.setMinutes(0);
+ rd0.setHours(0);
+ rd0.setMinutes(0);
+ if (Math.abs(d0 - rd0) <= (24 * 60 * 60 * 1000)) {
+ return '<div class="x-nice-dayofweek">' + _("Previous day") + '</div>';
+ }
+ }
+ }
+ return '<div class="x-nice-dayofweek">' + d.toLocaleString(tvheadend.language, {weekday: 'long'}) + '</div>' +
+ '<div class="x-nice-date">' + d.toLocaleDateString() + '</div>';
+}
+
/*
*
*/