The best part about subsonic is that it's easy to modify/hack to fit your purposes, sindre has done a great job of separating the logic from the layout, so you can easily modify it by editing various .jsp files in /WEB-INF/jsp/. I sorta hacked this in, but broke support for regular streaming (with .m3u/.pls). Basically you'll want to edit the playlist.jsp and force it into a "webplayer only mode". I posted elsewhere about switching in jwplayer, which is what this uses. You'll need to edit the xspfPlaylist.jsp if you want to take advantage of that. Hopefully this will give you some idea about how to do what you want. Just mess around with the code, you'll figure it out.
The way I have things setup the playlist editor is visible at the bottom, however the bottom frame is set to a slim view by default. To reveal it you have to click the resize icon, which reveals a scrollable playlist editor. Setup like this, you'll actually leave use webplayer by default off in settings (if you want to set it to always use a popup you can turn it on, but it'll disable the playlist). You can still detach normally, use playlists, load/save etc.
This script communicates with JW Player, is unneccesary, you can safely ignore that. I am still trying to figure out how to take advantage of the javascript API in JW Player. In terms of layout I've changed subsonic a bit, so keep that in mind, and take what you need. Using JW Player 3.16.
playlist.jsp
- Code: Select all
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
<html>
<head>
<%@ include file="head.jsp" %>
<title>subs0nic</title>
<script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
<c:choose>
<c:when test="${model.detached}">
<c:set var="width" value="400"/>
<c:set var="height" value="150"/>
</c:when>
<c:otherwise>
<c:set var="width" value="300"/>
<c:set var="height" value="20"/>
</c:otherwise>
</c:choose>
<script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
<c:choose>
<c:when test="${model.detached}">
</c:when>
<c:otherwise>
<script type="text/javascript" language="javascript">
function detach() {
popupSize("webPlayer.view?detached=",'player','toolbar=no,statusbar=no,location=no,scrollbars=no,width=400,height=150');
location.href = "empty.html";
}
</script>
</c:otherwise>
</c:choose>
</head>
<c:url var="playlistUrl" value="/xspfPlaylist.view">
<%-- Hack to force Flash player to reload playlist. --%>
<c:param name="dummy" value="${model.dummy}"/>
</c:url>
<c:choose>
<c:when test="${model.detached}">
<body width="400" height="150" onload="window.focus();window.moveTo(300, 200);"style="background: black; margin: 0 auto; padding: 0">
<div style="position: fixed; top: 0; left: 0; width: 395px; height: 140px; margin: 0 auto; padding: 0;">
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,28,0"
width="${width}" height="${height}">
<param name="allowScriptAccess" value="sameDomain"/>
<param name="movie" value="player.swf"/>
<param name="quality" value="high"/>
<param name="bgcolor" value="#000000"/>
<embed id="xspf_player" src="player.swf" quality="high" bgcolor="#000000" name="xspf_player" allowscriptaccess="sameDomain"
type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"
height="${height}" width="${width}" flashvars="file=${playlistUrl}&autostart=true&frontcolor=0x999999&lightcolor=0xfafafa&backcolor=0x000000&searchbar=false&enablejs=true&javascriptid=xspf_player&height=150&width=400&autoscroll=false&displaywidth=130&overstretch=true&thumbsinplaylist=false&showicons=false&fullscreen=false"></embed>
</object>
</div>
</c:when>
<c:otherwise>
<!-- This script communicates with JW Player -->
<script type="text/javascript">
// some variables to save
var currentPosition;
var currentRemaining;
var currentVolume;
var currentItem;
var currentState;
var currentLoad;
var currentXsize;
var currentYsize;
// this function is caught by the JavascriptView object of the player.
function sendEvent(typ,prm) { thisMovie("xspf_player").sendEvent(typ,prm); };
// these functions is called by the JavascriptView object of the player.
function getUpdate(typ,pr1,pr2,swf) {
if(typ == "time") { currentPosition = pr1; pr2 == undefined ? null: currentRemaining = Math.round(pr2); }
else if(typ == "volume") { currentVolume = pr1; }
else if(typ == "item") { currentItem = pr1; setTimeout("getItemData(currentItem)",100);}
else if(typ == "state") { currentState = pr1; }
else if(typ == "load") { currentLoad = pr1; }
else if(typ == "size") { currentXsize = "X=" + pr1; pr2 == undefined ? null: currentYsize = "Y=" + Math.round(pr2); }
var tmp = document.getElementById("pid"); if ((tmp)&&(swf != "null")) { tmp.innerHTML = "(received from the player with the id: <i><b>"+swf+"</b></i>)"; }
var tmp = document.getElementById("time"); if (tmp) { tmp.innerHTML = "<b>Time:</b> " + currentPosition + " <b>Remaining:</b> " + currentRemaining; }
var tmp = document.getElementById("volume"); if (tmp) { tmp.innerHTML = "<b>Volume:</b> " + currentVolume; }
var tmp = document.getElementById("item"); if (tmp) { tmp.innerHTML = "<b>Item:</b> " + currentItem; }
var tmp = document.getElementById("state"); if (tmp) { tmp.innerHTML = "<b>State:</b> " + currentState + " (0:ready/paused, 1:loading, 2:playing, 3:finished)"; }
var tmp = document.getElementById("load"); if (tmp) { tmp.innerHTML = "<b>Load:</b> " + currentLoad; }
var tmp = document.getElementById("size"); if (tmp) { tmp.innerHTML = "<b>Size:</b> " + currentXsize + ", " + currentYsize; }
};
function getItemData(idx) {
var obj = thisMovie("xspf_player").itemData(idx);
var tmp = document.getElementById("file"); if (tmp) { tmp.innerHTML = "<b>File:</b> " + obj["file"]; }
var tmp = document.getElementById("title"); if (tmp) { tmp.innerHTML = obj["title"]; }
var tmp = document.getElementById("link"); if (tmp) { tmp.innerHTML = "<b>Link:</b> " + obj["link"]; }
var tmp = document.getElementById("type"); if (tmp) { tmp.innerHTML = "<b>Type:</b> " + obj["type"]; }
var tmp = document.getElementById("id"); if (tmp) { tmp.innerHTML = "<b>Id:</b> " + obj["id"]; }
var tmp = document.getElementById("image"); if (tmp) { tmp.innerHTML = "<b>Image:</b> " + obj["image"]; }
var tmp = document.getElementById("author"); if (tmp) { tmp.innerHTML = "<b>Author:</b> " + obj["author"]; }
var tmp = document.getElementById("captions"); if (tmp) { tmp.innerHTML = "<b>Captions:</b> " + obj["captions"]; }
var tmp = document.getElementById("audio"); if (tmp) { tmp.innerHTML = "<b>Audio:</b> " + obj["audio"]; }
var tmp = document.getElementById("start"); if (tmp) { tmp.innerHTML = "<b>Start:</b> " + obj["start"]; }
var tmp = document.getElementById("category"); if (tmp) { tmp.innerHTML = "<b>Category:</b> " + obj["category"]; }
var tmp = document.getElementById("description"); if (tmp) { tmp.innerHTML = "<b>Description:</b> " + obj["description"]; }
var tmp = document.getElementById("latitude"); if (tmp) { tmp.innerHTML = "<b>Latitude:</b> " + obj["latitude"]; }
var tmp = document.getElementById("longitude"); if (tmp) { tmp.innerHTML = "<b>Longitude:</b> " + obj["longitude"]; }
var tmp = document.getElementById("city"); if (tmp) { tmp.innerHTML = "<b>City:</b> " + obj["city"]; }
var tmp = document.getElementById("date"); if (tmp) { tmp.innerHTML = "<b>Date:</b> " + obj["date"]; }
};
// These functions are caught by the feeder object of the player.
function loadFile(obj) { thisMovie("xspf_player").loadFile(obj); };
function addItem(obj,idx) { thisMovie("xspf_player").addItem(obj,idx); };
function removeItem(idx) { thisMovie("xspf_player").removeItem(idx); };
function getLength(swf) { return(thisMovie(swf).getLength()); };
// This is a javascript handler for the player and is always needed.
function thisMovie(movieName) {
if(navigator.appName.indexOf("Microsoft") != -1) {
return window[movieName];
} else {
return document[movieName];
}
}
</script>
<!-- actionSelected() is invoked when the users selects from the "More actions..." combo box. -->
<script type="text/javascript" language="javascript">
var N = ${fn:length(model.songs)};
var downloadEnabled = ${model.user.downloadRole ? "true" : "false"};
function actionSelected(id) {
if (id == "top") {
return;
} else if (id == "RepeatOn") {
location.href = "playlist.view?repeat";
} else if (id == "RepeatOff") {
location.href = "playlist.view?repeat";
} else if (id == "Shuffle") {
location.href = "playlist.view?shuffle";
} else if (id == "Clear") {
location.href = "empty.html";
} else if (id == "loadPlaylist") {
parent.frames.main.location.href = "loadPlaylist.view?";
} else if (id == "savePlaylist") {
parent.frames.main.location.href = "savePlaylist.view?";
} else if (id == "downloadPlaylist") {
location.href = "download.view?player=${model.player.id}";
} else if (id == "sortByTrack") {
location.href = "playlist.view?sortByTrack";
} else if (id == "sortByArtist") {
location.href = "playlist.view?sortByArtist";
} else if (id == "sortByAlbum") {
location.href = "playlist.view?sortByAlbum";
} else if (id == "selectAll") {
selectAll(true);
onSelectionChange();
} else if (id == "selectNone") {
selectAll(false);
onSelectionChange();
} else if (id == "remove") {
location.href = "playlist.view?remove=" + getSelectedIndexes();
} else if (id == "download") {
location.href = "download.view?player=${model.player.id}&indexes=" + getSelectedIndexes();
} else if (id == "Undo") {
location.href = "playlist.view?undo";
}
$("moreActions").selectedIndex = 0;
}
function getSelectedIndexes() {
var result = "";
for (var i = 0; i < N; i++) {
if ($("songIndex" + i).checked) {
result += (i + " ");
}
}
return result;
}
function selectAll(b) {
for (var i = 0; i < N; i++) {
$("songIndex" + i).checked = b;
}
}
function isSelectionEmpty() {
for (var i = 0; i < N; i++) {
if ($("songIndex" + i).checked) {
return false;
}
}
return true;
}
function onSelectionChange() {
var selectionEmpty = isSelectionEmpty();
var remove = $("moreActions").options["remove"];
remove.disabled = selectionEmpty ? "disabled" : "";
var download = $("moreActions").options["download"];
if (download) {
download.disabled = (selectionEmpty || !downloadEnabled) ? "disabled" : "";
}
}
</script>
<body id="footer">
<div id="footer_left">
<table width=116>
<tr>
<td width=16 valign=top valign=left><a href="javascript:parent.resizeFrame('42,*,190')"><img src="icons/bullet_arrow_up.png" title="Playlist"/></a></td>
<td width=100 valign=top valign=left>
<select id="moreActions" onchange="actionSelected(this.options[selectedIndex].id)">
<option id="top" selected="selected">Playlist</option>
<c:if test="${not model.player.clientSidePlaylist}">
<c:choose>
<c:when test="${model.repeatEnabled}">
<option id="RepeatOn"> Repeat [Off]</option>
</c:when>
<c:otherwise>
<option id="RepeatOff"> Repeat [On]</option>
</c:otherwise>
</c:choose>
</c:if>
<option id="Shuffle"> Shuffle</option>
<option id="loadPlaylist"> <fmt:message key="playlist.load"/></option>
<c:if test="${model.user.playlistRole}">
<option id="savePlaylist"> <fmt:message key="playlist.save"/></option>
</c:if>
<c:if test="${model.user.downloadRole}">
<option id="downloadPlaylist"> <fmt:message key="common.download"/></option>
</c:if>
<option id="sortByTrack"> <fmt:message key="playlist.more.sortbytrack"/></option>
<option id="sortByAlbum"> <fmt:message key="playlist.more.sortbyalbum"/></option>
<option id="sortByArtist"> <fmt:message key="playlist.more.sortbyartist"/></option>
<option disabled="disabled">--</option>
<option id="selectAll"> <fmt:message key="playlist.more.selectall"/></option>
<option id="selectNone"> <fmt:message key="playlist.more.selectnone"/></option>
<option id="remove"> <fmt:message key="playlist.remove"/></option>
<c:if test="${model.user.downloadRole}">
<option id="download"> <fmt:message key="common.download"/></option>
</c:if>
<option disabled="disabled">--</option>
<option id="Undo"> Undo</option>
<option id="Clear"> Clear</option>
</select>
</td>
</tr>
</table>
</div>
<div id="footer_right">
<div id="player">
<table style="border:0;margin:0auto;padding:0">
<tr>
<td align=right><div id="title"></div></td>
<td width=20><a href="javascript:detach()"><img src="/icons/detach.png" title="Detach"/></a></td>
<td width=300 align=right>
<div style=padding-top:4px;>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,28,0"
width="${width}" height="${height}">
<param name="allowScriptAccess" value="sameDomain"/>
<param name="movie" value="player.swf"/>
<param name="quality" value="high"/>
<param name="bgcolor" value="#000000"/>
<embed id="xspf_player" src="player.swf" quality="high" bgcolor="#000000" name="xspf_player" allowscriptaccess="sameDomain"
type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"
height="${height}" width="${width}" flashvars="file=${playlistUrl}&autostart=true&frontcolor=0x999999&lightcolor=0xfafafa&backcolor=0x000000&enablejs=true&javascriptid=xspf_player&showicons=false&fullscreen=false"></embed>
</object>
</div>
</td>
</tr>
</table>
</div>
</div>
<div id="playlist_editor">
<c:choose>
<c:when test="${empty model.songs}">
<div id="empty_playlist">Playlist is Empty.</div>
</c:when>
<c:otherwise>
<table width=100% style="text-align: left;border-collapse:collapse;white-space:nowrap;">
<c:set var="cutoff" value="${model.visibility.captionCutoff}"/>
<c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
<c:set var="i" value="${loopStatus.count - 1}"/>
<tr style="margin:0;padding:0;border:0">
<td width=1></td>
<td width=16><a name="${i}" href="playlist.view?remove=${i}"><img width="16" height="16" src="<spring:theme code="removeImage"/>"
alt="<fmt:message key="playlist.remove"/>"
title="<fmt:message key="playlist.remove"/>"/></a></td>
<td width=16><a href="playlist.view?up=${i}"><img width="16" height="16" src="<spring:theme code="upImage"/>"
alt="<fmt:message key="playlist.up"/>"
title="<fmt:message key="playlist.up"/>"/></a></td>
<td width=16><a href="playlist.view?down=${i}"><img width="16" height="16" src="<spring:theme code="downImage"/>"
alt="<fmt:message key="playlist.down"/>"
title="<fmt:message key="playlist.down"/>"/></a></td>
<sub:url value="main.view" var="mainUrl">
<sub:param name="path" value="${song.musicFile.parent.path}"/>
</sub:url>
<c:choose>
<c:when test="${i % 2 == 0}">
<c:set var="class" value="class='bgcolor2'"/>
</c:when>
<c:otherwise>
<c:set var="class" value=""/>
</c:otherwise>
</c:choose>
<td width=16 style="padding-left: 0.1em"><input type="checkbox" class="checkbox" id="songIndex${i}" onchange="onSelectionChange()"/></td>
<td ${class} style="padding-right:0.25em"/>
<c:if test="${model.visibility.trackNumberVisible}">
<td ${class} style="padding-right:0.5em;text-align:right">
<span class="detail">${song.musicFile.metaData.trackNumber}</span>
</td>
</c:if>
<td ${class} style="padding-right:1.25em">
<c:choose>
<c:when test="${model.player.clientSidePlaylist}">
<span style="font-size:.9em; line-height: 1.25em;"" title="${song.musicFile.metaData.title}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(song.musicFile.title)}</str:truncateNicely></span>
</c:when>
<c:otherwise>
<a href="javascript:sendEvent('playitem',${i})" title="${song.musicFile.metaData.title}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(song.musicFile.title)}</str:truncateNicely></a>
</c:otherwise>
</c:choose>
</td>
<c:if test="${model.visibility.albumVisible}">
<td ${class} style="padding-right:1.25em">
<span class="detail" title="${song.musicFile.metaData.album}"><a target="main" href="${mainUrl}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(song.musicFile.metaData.album)}</str:truncateNicely></a></span>
</td>
</c:if>
<c:if test="${model.visibility.artistVisible}">
<td ${class} style="padding-right:1.25em">
<span class="detail" title="${song.musicFile.metaData.artist}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(song.musicFile.metaData.artist)}</str:truncateNicely></span>
</td>
</c:if>
<c:if test="${model.visibility.genreVisible}">
<td ${class} style="padding-right:1.25em">
<span class="detail">${song.musicFile.metaData.genre}</span>
</td>
</c:if>
<c:if test="${model.visibility.yearVisible}">
<td ${class} style="padding-right:1.25em">
<span class="detail">${song.musicFile.metaData.year}</span>
</td>
</c:if>
<c:if test="${model.visibility.formatVisible}">
<td ${class} style="padding-right:1.25em">
<span class="detail">${fn:toLowerCase(song.musicFile.metaData.format)}</span>
</td>
</c:if>
<c:if test="${model.visibility.fileSizeVisible}">
<td ${class} style="padding-right:1.25em;text-align:right">
<span class="detail"><sub:formatBytes bytes="${song.musicFile.metaData.fileSize}"/></span>
</td>
</c:if>
<c:if test="${model.visibility.durationVisible}">
<td ${class} style="padding-right:1.25em;text-align:right">
<span class="detail">${song.musicFile.metaData.durationAsString}</span>
</td>
</c:if>
<c:if test="${model.visibility.bitRateVisible}">
<td ${class} style="padding-right:0.25em">
<span class="detail">
<c:if test="${not empty song.musicFile.metaData.bitRate}">
${song.musicFile.metaData.bitRate} Kbps ${song.musicFile.metaData.variableBitRate ? "vbr" : ""}
</c:if>
</span>
</td>
</c:if>
<td width=4></td>
</tr>
</c:forEach>
</table>
</c:otherwise>
</c:choose>
</div>
</c:otherwise>
</c:choose>
</body>
</html>
webPlayer.jsp
- Code: Select all
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
<html>
<head>
<%@ include file="head.jsp" %>
<title>subs0nic</title>
<c:set var="width" value="400"/>
<c:set var="height" value="150"/>
<link rel="stylesheet" href="style/black.css" type="text/css"/>
<script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
<script type="text/javascript" language="javascript">
function detach() {
popupSize("webPlayer.view?detached=",'player','toolbar=no,statusbar=no,location=no,scrollbars=no,width=400,height=150');
}
</script>
<script type="text/javascript">
var currentItem;
function getUpdate(typ,pr1,pr2,pid) {
if(typ == "item") { currentItem = pr1; setTimeout("getItemData(currentItem)",100); }
};
function getItemData(idx) {
var obj = thisMovie("xspf_player").itemData(idx);
document.getElementById("songinfo").innerHTML = obj["title"];
};
// This is a javascript handler for the player and is always needed.
function thisMovie(movieName) {
if(navigator.appName.indexOf("Microsoft") != -1) {
return window[movieName];
} else {
return document[movieName];
}
};
</script>
</head>
<c:url var="playlistUrl" value="/xspfPlaylist.view">
<%-- Hack to force Flash player to reload playlist. --%>
<c:param name="dummy" value="${model.dummy}"/>
</c:url>
<c:url var="playerUrl" value="player.swf?playlist_url=${playlistUrl}&autostart=true"/>
<c:choose>
<c:when test="${model.detached}">
<body width="400" height="150" onload="window.focus();window.moveTo(300, 200);"style="background: black; margin: 0 auto; padding: 0">
<div style="position: fixed; top: 0; left: 0; width: 395px; height: 140px; margin: 0 auto; padding: 0;">
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,28,0"
width="${width}" height="${height}">
<param name="allowScriptAccess" value="sameDomain"/>
<param name="movie" value="player.swf"/>
<param name="quality" value="high"/>
<param name="bgcolor" value="#eee"/>
<embed id="xspf_player" src="player.swf" quality="high" bgcolor="#222222" name="xspf_player" allowscriptaccess="sameDomain"
type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"
height="${height}" width="${width}" flashvars="file=${playlistUrl}&autostart=true&frontcolor=0x999999&lightcolor=0xfafafa&backcolor=0x000000&searchbar=false&height=150&width=400&autoscroll=false&displaywidth=130&overstretch=true&thumbsinplaylist=false&showicons=false&fullscreen=false"></embed>
</object>
</div>
</c:when>
<c:otherwise>
<body onLoad="detach()" class="bgcolor3" style="margin: 0 auto; border-top: solid 4px #181A18; font-size: .8em; color: #999;">
<div style="float:right; margin: 0 auto; padding-right: 6px; padding-top: 4px;">
</div>
</c:otherwise>
</c:choose>
</body>
</html>
xspfPlaylist.jsp
- Code: Select all
<%@ include file="include.jsp" %>
<%@ page language="java" contentType="text/xml; charset=utf-8" pageEncoding="iso-8859-1" %>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<c:forEach var="song" items="${model.songs}">
<sub:url value="/stream" var="streamUrl">
<sub:param name="path" value="${song.musicFile.path}"/>
</sub:url>
<sub:url value="coverArt.view" var="coverArtUrl">
<sub:param name="size" value="200"/>
<c:if test="${not empty song.coverArtFile}">
<sub:param name="path" value="${song.coverArtFile.path}"/>
</c:if>
</sub:url>
<track>
<location>${streamUrl}</location>
<image>${coverArtUrl}</image>
<title>${song.musicFile.metaData.artist} - ${song.musicFile.title}</title>
<meta rel="type">mp3</meta>
</track>
</c:forEach>
</trackList>
</playlist>
Again, This will break normal streaming, all music will be played from embedded player. Pretty ugly, but hey, does what I want,
