In meinem Job als Ext JS Consultant bin ich häufig unterwegs. Während meiner Reisezeit höre ich gerne Hörbücher, von denen Spotify zum Glück, eine große Auswahl an ungekürzten Exemplaren anbietet. Ein Hörbuch-Album besteht in der Regel aus dutzenden oder sogar hunderten von Titeln.
Nach langen Arbeitstagen höre ich auch gerne mal gute Musik, um wieder runter zu kommen. Glücklicherweise gibt es bei Spotify auch davon mehr als genug und so wechsele ich einfach zu meiner Lieblings-Playlist.
Wenn ich nach dem Musikgenuss das nächste Mal in mein Hörbuch-Album wechsele, ist meine letzte Hörposition allerdings verloren und ich muss, basierend auf meiner Erinnerung, nach der richtigen Stelle suchen. Das ist so, weil Spotify keine Lesezeichen-Funktion zum Speichern der Position in den Titeln anbietet. Es ist nicht wirklich überraschend, das ich nicht der Einzige mit diesem Problem bin. Schaut man in die Spotify-Community-Foren, findet man viele Posts und Kommentare von Leuten die das gleiche Problem haben. Leider bleibt das Verlangen nach dieser Funktion, durch Spotify, schon seit über zwei Jahren ungehört.
Es ist sehr nervend und auch nicht ganz einfach jedes Mal händisch nach der richtigen Position zu suchen. Typischerweise benötigt man dafür drei Informationsbestandteile:
Im Moment sind mir drei Wege bekannt wie Spotify-Benutzer das Problem für sich lösen.
Meine Lösung basiert auf einer Ext JS Web-App, welche die Spotify Web API verwendet, um auf die Informationen meiner gehörten Spotify-Titel zuzugreifen.
Name der App: «Bookmarks for Spotify»
URL der App: https://bookmarks-for-spotify.ws4.be
Die App besitzt folgende Features:
Meine App läuft auf einem Node.js Server. Die serverseitige Applikation liefert den Ext JS App Client aus und stellt für diesen Schnittstellen bereit, um mit der Spotify API zu kommunizieren. Die Spotify API REST Requests werden von der Node.js App ausgeführt.
Die Client App basiert mit Ext JS 6.5 auf der momentan aktuellen Version des Frameworks. Es verwendet das «modern Toolkit» und wurde generiert und gebuildet mit Sencha Cmd. Das Theme wurde mit dem Sencha Themer erstellt. Ich verwende therootcause.io für das Error Tracking. Für das Hosting habe ich ein Docker Image erstellt und lasse den Container auf sloppy.io laufen. Für den Build- und Deploy-Prozess habe ich ein Set an NPM Scripts geschrieben.
Technologie Stack:
Zu Beginn der Entwicklung habe ich mit Hilfe von Sencha Cmd einen Sencha Workspace und das Grundgerüst meiner Spotify App generiert.
sencha generate app -modern Spotify client/
Während der Entwicklung verwende ich Sencha Cmd um die Dateiabhängigkeiten aufzulösen und die Bootstrap- sowie die Styling-Dateien zu generieren.
sencha app watch
Meine build.xml habe ich erweitert, so dass die app.json Versionsnummer basierend auf der aus der package.json gesetzt wird. Des Weiteren werden entwicklerspezifische Werte in der index.html gesetzt. Dies erlaubt mir, die App-Versionsnummer an einer Stelle via npm zu setzen und dann an mehren Stellen, beispielsweise zur Anzeige in der App oder für das Error Tracking, zu verwenden.
Hierfür sind in der app.json und der index.html Datei-Platzhalter definiert, welche während des Builds ersetzt werden.
{
...
/**
* The version of the application.
*/
"version": "@@@version@@@",
...
}
Ich verwende das «-after-init» Target in der build.xml, um die Platzhalter durch Werte der package.json und config.json zu ersetzen.
<target name="-after-init">
<!-- Script to get properties from JSON file -->
<x-script-def name="get-prop">
<attribute name="file"/>
<attribute name="query"/>
<attribute name="property"/>
<script src="${cmd.dir}/ant/JSON.js"/>
<script src="${cmd.dir}/ant/ant-util.js"/>
<![CDATA[
var fileName = attributes.get('file') + '';
var query = attributes.get('query') + '';
var property = attributes.get('property') + '';
var object = readJson(fileName);
var s;
query = query.split('.');
while (query.length) {
s = query.shift();
object = object[s];
}
project.setNewProperty(property, object);
]]>
</x-script-def>
<!-- Read values and save in vars -->
<get-prop file="../package.json" query="version" property="package_version"/>
<get-prop file="../config.json" query="therootcauseapplicationid" property="therootcauseapplicationid"/>
<get-prop file="../config.json" query="googleanalytics" property="googleanalytics"/>
<get-prop file="../config.json" query="frameworkversion" property="frameworkversion"/>
<!-- Replace placeholders -->
<replace file="app.json" token="@@@version@@@" value="${package_version}"/>
<replace file="index.html" token="@@@version@@@" value="${package_version}"/>
<replace file="index.html" token="@@@therootcauseapplicationid@@@" value="${therootcauseapplicationid}"/>
<replace file="index.html" token="@@@googleanalytics@@@" value="${googleanalytics}"/>
<replace file="index.html" token="@@@frameworkversion@@@" value="${frameworkversion}"/>
</target>
Die Ext JS App verwendet das «Modern Toolkit» und liegt in der von Sencha Cmd generierten Ordnerstruktur. Es werden das MVC und MVVM Pattern, so wie ES6 Code Style verwendet. ES6 wird ab Sencha Cmd Version 6.5 unterstützt.
Binding wird an zahlreichen Stellen im Code verwendet. Ein üblicher Anwendungsfall sind Stores. Des Weiteren wird es in Kombination mit ViewModel Formulas benutzt. Im unten stehenden Beispiel prüft eine Formula im ViewModel ob das Authentifizierungstoken gesetzt ist. Basierend auf der Information {hasToken} oder {!hasToken} werden der Login Button und die Titelliste ein oder ausgeblendet.
View: view/main/Main.js
...
items: [
{
xtype: 'spotify-login',
bind : {
hidden: '{hasToken}'
}
},
{
xtype : 'spotify-recentlyplayed',
bind : {
hidden: '{!hasToken}',
store : '{playedTracks}'
},
...
ViewModel: view/main/MainModel.js
...
data: {
token: ''
},
formulas: {
// check if token is set
hasToken: function (get) {
return !(get('token') === '');
}
},
...
Im CurrentTrackModel formatiert eine Formula die Millisekunden-Werte der bereits gespielten Zeit und der Gesamtzeit in das Format "mm:ss".
ViewModel: view/tracks/currenttrack/CurrentTrackModel.js
...
data : {
currentPlayback: null
},
formulas: {
// format progress ms to "00:00"
progress_ms: function (get) {
const ms = get('currentPlayback.progress_ms');
return parseInt(ms / 1000 / 60) + ":" + parseInt(ms / 1000 % 60);
},
...
Mit Ext JS 6.5 kann man im View Controller einfach auf Datenveränderungen des View Models reagieren. Durch die Verwendung der bindings Konfiguration wird im folgendem Beispiel die onChangeToken Methode aufgerufen. Ich verwende diese, um den aktuell spielenden Titel zu laden, sobald das Authentifizierungstoken gesetzt wurde.
ViewController: view/tracks/currenttrack/CurrentTrackController.js
...
bindings: {
onChangeToken: {
token: '{token}'
}
},
/**
* when token changes, trigger load of current playback
*
* @param data
*/
onChangeToken(data){
if (data.token) {
this.loadCurrentPlayback()
}
},
...
Es ist sogar möglich mathematische Berechnungen mit Bindings zu tätigen. In der «aktuelle Spielender Titel»-Komponente verwende ich dieses Feature, um den Fortschritt des Fortschrittsbalken zu berechnen.
View: view/tracks/currenttrack/CurrentTrack.js
{
xtype: 'progress',
bind: {
value: '{currentPlayback.progress_ms / currentPlayback.item.duration_ms}',
}
}
Die App enthält zwei Titel-Listen. Eine für die Darstellung der zuletzt gespielten Titel und eine der gemerkten Titel (Lesezeichen). Das Aussehen beider ist gleich, aber die Aktionen bei Events unterscheiden sich. Aus diesem Grund gibt es eine abstrakte Basis-Klasse, welche das XTemplate enthält. Die «zuletzt gespielte Titel Liste» und «gemerkte Titel Liste» erweitern die abstrakte Liste. Die «zuletzt gespielte Titel Liste» erweitert die Funktionalität um das «Pull to Refresh»-Plugin, während die «gemerkte Titel Liste» nur einen Text ergänzt, für den Fall, dass die Liste leer bleibt. Beide Komponenten werden im Main View eingebunden und mit unterschiedlichen Event Listener verknüpft.
View: view/tracks/List.js
...
itemTpl:
'<div class="track hbox ">' +
// bookmark/ed icon -> trigger to bookmark or remove bookmark
'<div class="track-bookmark hbox cross-center main-center ">' +
// conditional bookmark icon rendering based on the bookmarked flag of the record
'<span class="icon x-fa {[values.bookmarked ? "fa-bookmark" : "fa-bookmark-o"]}" />' +
'</div>' +
// track infos
'<div class="track-info flex">' +
// date formatting
'<span class="track-played-at">{played_at:date("d.m.Y - H:i")}</span>' +
'<br /> {name} - {artist} ({progress_ms_display}/{duration_ms_display})' +
'</div>' +
// play icon -> trigger for playback
'<div class="track-play hbox cross-center main-center ">' +
'<span class="icon x-fa fa-play-circle-o" />' +
'</div>' +
'</div>'
...
Für jeden Titel, in den Listen, gibt es zwei mögliche Aktionen «als Lesezeichen merken» oder «Titel abspielen». Für beide wird der itemtap Event benutzt. Um die Aktionen zu unterscheiden, verwenden wir die getTarget() Methode des itemtap Listener Event Objekts. Sie überprüft, ob ein DOM Element mit einer bestimmten CSS Klasse getappt wurde.
ViewController: view/main/MainController.js
...
onItemTap(grid, index, target, record, e) {
if (e.getTarget('.track-bookmark')) {
// bookmark track
}
if (e.getTarget('.track-play')) {
// start playback track
}
},
...
Man kann sehen, dass dies die CSS-Klassen des Listen XTemplates sind.
In der App gibt es drei Komponenten, welche das Abspielen eines Titels anstoßen. Ich verwende ein eigenes Event, um die Information des zu startenden Titels über den View Controller zu verbreiten.
ViewController: view/tracks/currenttrack/CurrentTrackController.js
...
playCurrentTrack() {
const vm = this.getViewModel();
this.fireEvent('playCurrentTrack', vm.get('currentPlayback'));
}
...
Im Main View Controller sind Event Listener definiert, welche auf das Event reagieren und das Abspielen des Titels anstoßen.
ViewController: view/main/MainController.js
...
listen: {
controller: {
'*': {
bookmarkCurrentTrack: 'onBookmarkCurrentTrack',
playCurrentTrack : 'onPlayCurrentTrack'
}
}
},
...
Manche Daten der App werden persistiert, so dass sie sessionübergreifend zur Verfügung stehen. Die persönlich gespeicherten Lesezeichen der Titel gehören hierzu, denn diese möchte der Benutzer natürlich beim nächsten Öffnen oder nach einem Refresh der App wieder vorfinden. Der bookmarked Store benutzt den Localstorage Proxy, um die Daten im Localstorage des Browsers zu persistieren.
ViewModel: view/main/MainModel.js
...
bookmarked : {
autoLoad: true,
storeId : 'bookmarked',
model : 'Spotify.model.BookmarkedTrack',
proxy : {
type: 'localstorage',
id : 'bookmarked-tracks'
}
}
...
Um ein Login nach jedem Refresh zu verhindern, wird das Authentifizierungstoken von Spotify auch im Localstorage persistiert.
In meinen Ext-JS-Applikationen zeige ich gerne die App-Versionsnummer aus der app.json-Datei an. Ich verwende diese, um herauszufinden welche Version auf welcher Stage läuft, als Info beim Error Tracking und damit Benutzer gezieltes Feedback zu einer Version geben können. In der Bookmarks for Spotify App findet man sie im Info View. Hierfür muss man nur die Ext.manifest.version an einer beliebigen Stelle in seiner App anzeigen.
{
xtype : 'container',
html : 'App v' + Ext.manifest.version
}
Mit Sencha Themer habe ich, auf Basis des Material Design Themes, ein Ext JS Theme Package erstellt und in meiner App eingebunden.
Publish > Apply Theme to App(s)...
Ausgehend von der $base-color, wurden Farben und Größen angepasst, um ein an Spotify angelehntes Aussehen, zu erreichen. Über das Interface des Sencha Themer lassen sich die Theme-Variablen leicht anpassen, um das Theme zu individualisieren.
Für spezielles Styling, habe ich benutzerspezifische UIs angelegt. Zum Beispiel das Spotify UI, welches vom Login-Button verwendet wird. Manchmal ist es nötig, im Sencha Themer zur SASS Variable Anzeige zu wechseln, um Variablen zu ändern. Im folgenden Bild, kann man sehen, dass ich dies getan habe, um den Wert von $ui-spotify-button-padding-big zu ändern.
Im Code können diese UIs über die ui-Konfiguration verwendet werden.
{
xtype : 'button',
ui : 'spotify',
iconCls: 'x-fa fa-spotify',
text : 'Login with Spotify',
handler: 'onSpotifyLogin',
width : 280
}
Für den Spotify-Login-Button und an anderen Stellen habe ich Font-Icons verwendet. Im oben stehenden Code sieht man das iconCls
Font Awesome verwendet, um das Spotify Icon anzuzeigen. Um Font Awesome in einer App zu verwenden, muss das "font-awesome" Package in der app.json Datei „required“ werden. Auf http://fontawesome.io/icons/ kann man nach benötigten Icons suchen und dann den angezeigten CSS-Klassennamen mit x-fa im Code kombinieren. Beispiel:
iconCls: 'x-fa fa-spotify'
Für die von mir erstellten Komponenten gibt es extra SCSS im Theme Package. Wenn kein namespace in der package.json gesetzt wurde, kann das SCSS anlog zu Ext JS Komponenten Struktur abgelegt werden. Siehe Bild:
Das Styling der Ext-JS-Komponente für die Anzeige des aktuellen Titels mit dem Klassennamen Spotify.view.tracks.currenttrack.CurrentTrack und dem Pfad:
/bookmarks-for-spotify/client/app/view/tracks/currenttrack/CurrentTrack.js
kann unter folgendem Pfad gefunden werden:
/bookmarks-for-spotify/client/packages/local/spotify/modern/sass/src/Spotify/view/tracks/currenttrack/CurrentTrack.scss
man sieht das
.../view/tracks/currenttrack/CurrentTrack.js
zu
.../view/tracks/currenttrack/CurrentTrack.scss
passt.
Hier bei der dkd haben wir komplexe Continuous Integration und Continuous Deployment Prozesse um Apps und Webseiten bis zur Produktions-Stage zu bringen. Wir verwenden hierfür Tools wie Phabricator, Jenkins und Platform.sh. Inspiriert durch diese Prozesse, war es mein Ziel einen einfachen automatisierten Weg zu haben, um meine App zu Builden, Deployen und laufen zu lassen.
Mein Prozess sieht wie folgt aus:
Zu diesem Zweck habe ich ein Set an npm Skripten erstellt, welche all dies machen und durch einen einfachen Befehl auf dem Terminal ausgeführt werden können:
APP_VERSION=2.3.0 DOMAIN=bookmarks-for-spotify.ws4.be DOCKERHUB_REPOSITORY=mrsunshine/spotify-recently-played-tracks npm run deploy:prod
Durch die Verwendung mir bekannter Frameworks und Tools konnte ich das Problem einfach und elegant lösen. Den Code der App habe ich auf Github veröffentlicht.
Ich freue mich die Bookmarks for Spotify - https://bookmarks-for-spotify.ws4.be App mit dir zu teilen. Ich verwende sie jeden Tag, um zwischen meinen Hörbüchern und Musik-Playlisten zu wechseln und hoffe, dass sie auch dir hilft!
Hallo, perfekt, vielen Dank, danach habe ich schon lange gesucht. Wäre es möglich dies über einen Jailbreak tweak in die Spotify App auf dem iPhone zu integrieren? Viele Grüße und bleib gesund!!!Claudio