New versions of Angular come out every few months. Every once in a while I regenerate Shufflizer to catch up. I want to make sure Shufflizer is not using depricated features. Shufflizer is Angular v17.2 now.
Transaction integrity improved
In my previous post, I proposed some enhancements to improve Shufflizer’s transaction integrity. These are done and in production now. They are:
- The count of songs is confirmed on download and upload
- A temporary backup copy of the playlist is uploaded when doing same-playlist upload, and then deleted only if the same-playlist upload succeeds
I just wound up doing more callbacks
Most of the enhancements needed procedural (aka. synchronous) processing. “Don’t do the following step, and don’t accept user input, until the current step finishes ok.” I looked for something in ReactiveX’s JavaScript implementation to do this (RXJS), but did not find one. Perhaps JavaScript’s await would have done it, but I realized that I could:
- make steps run in series using the usual approach – callbacks
- prevent user input with something I already had – the loading spinner
So I proceeded with additional layers of callbacks and moved my command that closes the loading spinner to the bottom layer.
What I mean by a callback is a procedure that is called after the Observable, which is asynchronous, returns a value. In this example deletePlaylist “calls back” to getPlaylists. getPlaylists does not run until deletePlaylist returns a value:
this.spotSvc.deletePlaylist(b_plId,accessToken).subscribe(()=>this.getPlaylists(userId, userCountry, accessToken));
Transaction integrity
We do not have ACID transaction integrity
Shufflizer is built on the Spotify Web API. This API does not provide ACID transaction integrity. If the user’s computer crashes in the middle of an upload, or if the user simply shuts the window in the middle of an upload, the uploaded playlist will be incomplete. If the user has two sessions going (ie. two separate windows on the same device or perhaps on different devices) and is making changes to playlists in each, it is possible that uploading could lose or duplicate songs.
When uploading to a new playlist these concerns are not a big deal, but Shufflizer’s default is to upload to the same playlist. Plus Shufflizer is meant for big playlists. The bigger the playlist the bigger the bummer if it gets ruined. (It is possible to enlist the help of Spotify technical support personnel to restore a playlist to a prior state, but obviously this is an undesirable last resort.)
Here are some things that Shufflizer could do to reduce risk
Playlist initial download:
- After downloading a playlist for edit, confirm that the count of songs reported by Spotify agrees with the count of songs downloaded.
Same-playlist upload:
- first upload as a new playlist, creating a temporary backup copy
- then do the normal upload, rewriting the original playlist
- now confirm that the count of songs reported by Spotify agrees with the count of songs uploaded
- if all is well, delete the new playlist (the temporary backup copy)
With these measures in place, if something goes wrong the user will wind up with two versions of the playlist. Either the original playlist will be good or the backup version will be good, or even both could be good depending on the nature of the outage. It’s unlikely that both will be bad.
Why not just make Shufflizer always upload to a new playlist? Followers and clutter. Same-playlist upload retains a playlist’s followers. Same-playlist upload avoids the clutter of a great many similar playlists that would result from frequent use of Shufflizer.
ReactiveX Observables
I will begin working on these enhancements. I tagged this as a TypeScript and Angular blog entry because I think this will take me a bit deeper into
Tabs, and some groundwork for testing
Shufflizer now sports Angular Material tabs along the bottom edge of the window. This provides a more conventional way to navigate between the app’s pages and also gives me a place to implement an options/features page when I get to working on that.
In addition, under the hood I put all the http calls to Spotify into their own service. This is what the Angular documentation’s HTTP page recommends. It helped me implement better error handling, and better positions the code to take advantage of Jasmine and Karma testing (which I learned how to do at the Angular Denver 2018 conference, and am looking forward to trying out in Shufflizer).
One other thing: immediately upon upload Shufflizer will start playing the playlist in Spotify, if you have a device active, running Spotify.
Angular 6 Shufflizer
Shufflizer has been updated. There are no new features.
This version of Shufflizer was created with Angular 6. Some obsolete sections of code have been removed. All layouts have been changed from old style html tables to Angular flex-layout (previously it was a mish-mash of these two approaches).
Known issue
Playlist selection does not work on Safari/iPad when the user has enough playlists that the dropdown picklist needs to scroll. This seems to me to be an Angular Material bug. I am considering a more luxury playlist selection interface, and am motivated to make it a priority since it would address this known issue.
Global constants
One thing that was bugging me in my flex-layout performance solution was how I defined the global constants for my three layouts PHONE, TABLET, and HD. As always with constants, I’m just trying to make the code clearer to the reader. There is nothing more to this than PHONE is 1, TABLET is 2, and HD is 3.
We are not able to use the const declaration inside of class definitions.
const sizeNum = {PHONE:1,TABLET:2,HD:3}
Attempting this results in:
error TS1248: A class member cannot have the 'const' keyword.
We could define the constants outside of the class by putting the line of code just above the class, but this isn’t good encapsulation, and code fragments in the html template cannot see the constants. With the constants defined outside of the class, something like this is impossible:
<div *ngIf="activeSize()>sizeNum.PHONE">
Because of these issues, I resorted to a plain old hardcoded object, defined as usual inside of the class:
sizeNum: Object = {PHONE:1,TABLET:2,HD:3}
This doesn’t set up true constants. It is possible to programmatically change the sizeNum object later (eg. sizeNum[‘PHONE’]=5). Also, they are not global, and so I wound up having to pass the sizeNum object as a parameter to other components. Awkward.
There is a better way.
Angular dependancy injection
I am accustomed to using dependancy injection for full fledged classes, but it also can inject simpler objects. This works nicely for global constants. Set them up in app.module.ts:
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { PlaylstService } from './playlst.service'; ... const sizeNum = {PHONE:1,TABLET:2,HD:3} @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, ... ], providers: [ PlaylstService, { provide:'SizeConstants',useValue:sizeNum } ], bootstrap: [AppComponent] }) export class AppModule { }
Put them into the constructor in desired components:
import { Component, Inject } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; ... import { PlaylstService } from './playlst.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { accessToken: string; // access token for the session. we grep this outta the url ... constructor ( private http: HttpClient, private plistSvc: PlaylstService, @Inject('SizeConstants') public szNum: any ) { // initializations try {this.accessToken=window.location.hash.match(/^#access_token=([^&]+)/)[1];} catch (ERR) {this.accessToken='BAD';} ...
Make use of them, no problem even in html code fragments:
<div *ngIf="activeSize()>szNum.PHONE">
Animation
In web app development, animation is automation of the position and display characteristics of DOM elements. Angular provides its own programming interface for animations, insulating the developer from browser compatibility concerns. The developer defines styles for different states a DOM element can take on, and defines transitions for how to change from one state to another. Let’s first look at a non-animated example, and then animate it.
Hard cut
Say we have component with a variable named mood. Sometimes it holds the value “friendly” and sometimes it holds the value “mad.” In the component’s html template we display the value, but additionally we want a red background if mad. So far none of this requires Angular animation. We can use stock Angular to set the background color based on the mood value.
<p [style.background-color]=”mood==’mad’?’red’:’green'”>{{mood}}</p>
mad
This creates what Rachel Nabors calls a hard cut. This term comes from video/film editing and means there is no transition from one scene to the next. The first scene ends and the next scene appears abruptly. In our case, the moment the value for mood changes, the background color instantly changes. The text does too, but for simplicity let’s just focus our work on the background color.
Crossfade
We can introduce Angular animation to create a transition instead of a hard cut. Say we want the colors to slowly transition for three seconds when the mood changes state, a rudimentary animation known in video/film editing as a crossfade.
Instead of directly setting the style for mad, we define an animation trigger. In the trigger we define the style for mad (red) and the style for friendly (green), and how to animate the transition (take a leisurely 3 seconds to change color instead of doing it instantly). I have named the animation trigger moodAni.
<p [@moodAni]=’mood’>{{mood}}</p>
mad
In Angular Typescript, in addition to the usual @Component selector and template, we define animations. The <=> means do the transition when going either way, from mad to friendly or from friendly to mad:
@Component({ selector: 'app-root', template: '<p [@moodAni]="mood">{{mood}}</p>', animations: [ trigger('moodAni', [ state('mad',style({backgroundColor:'red'})), state('friendly',style({backgroundColor:'green'})), transition('friendly <=> mad',animate('3s')) ] ) ] })
Crossfade?
The term “crossfade” isn’t in the code. Any transition for an element that isn’t changing size or position is a crossfade. More generally, what the Angular animation module does is known as tweening. We define states or keyframes and then specify how long to take between each. This is quite a bit like CSS transitions. Indeed, you might be wondering how I managed to get Angular examples working directly inside this blog. I didn’t. They are pure CSS equivalents.
Nice corral you got there, Angular
If you haven’t made the plunge yet into Angular animations, you might be wondering if you’d be better off sticking with CSS transitions. It is possible. It’s what I was doing before I discovered Angular animations. I like the Angular way better. I don’t have to worry about browser differences, and the coding for animation is corralled into the component’s animations specification.
But some horses can get through that fence
Notice in the crossfade example above that if you click the button a second time quickly, the new animation immediately takes over. In Shufflizer I have a case where this isn’t desirable — the download progress spinner. For small playlists sometimes it would not fade smoothly in and out. When the download was done before the fade-in was done, the progress spinner would cut over to the fade-out animation. Nothing was technically wrong, but it was strange and unpolished. Here is how I solved it.
Angular provides an event that fires at the beginning and the end of an animation. I use this event to prevent shutting the progress spinner while the fade-in animation is still running.
<div class="dimmed" style="text-align:center;padding-top:100px;padding-left:20%;padding-right:20%" *ngIf="dimSet()" [@dimAnim] (@dimAnim.start)="setAnim($event)" (@dimAnim.done)="setAnim($event)"> <mat-card class="webLandscape"> <mat-card-content> <h2 class="example-h2">{{plLoadingDirection()}}loading... {{progressPcnt()}}%</h2> <mat-progress-spinner mode="determinate" [value]="progressPcnt()" style="margin-left:auto;margin-right:auto"> </mat-progress-spinner> </mat-card-content> </mat-card> </div>
The progress spinner renders when dimSet() is true. setAnim simply stashes the event object into a variable so dimSet can use it.
It all comes down to the last line in dimSet. Return true if a playlist is loading or if the fade-in animation is still running.
setAnim(ev): void {
this.anim=ev;
}
dimSet(): boolean {
let animStillRunning=this.anim
&& this.anim.triggerName == 'dimAnim'
&& this.anim.totalTime > 1
&& this.anim.phaseName == 'start'
&& this.anim.fromState == 'void' ;
return this.playlistService.plIsLoading
|| animStillRunning;
}
I found the names of the event attributes on the AnimationEvent documentation page.
So I like the animations “corral” but when we need to know the status of an animation, it’s not an animation we can just set into motion and forget about, then special coding for animations comes back into our TypeScript code.
For more information about Angular animation, see the Angular animations page. It’s where I learned most of what I know about Angular animations.
Session timer solution
In my previous blog post I described a flaw in my implementation of a session timer. Spotify doesn’t provide a way to monitor how much time is left but we know that the session lasts for sixty minutes, so I start my own sixty minute timer. My problem was that I was inadvertently creating more than one.
A singleton timer
My solution is to create a singleton timer, by subscribing only once. Instead of my class exposing the observable, it exposes the results – a simple numeric variable for the minutes remaining:
Observable.timer(0, 60000)
.map(mins => 60 - mins)
.takeWhile(minsRemaining => minsRemaining >= 0)
.subscribe(x => this.minsRemaining=x);
In the HTML, I rely on Angular to automatically detect changes to minsRemaining. Here is the button disable expression now. The async pipe subscribe is gone:
<button (click)="uploadToSpotify()" mat-raised-button
[disabled]="!playlistIsEdited()||minsRemaining<=0">
<i class="material-icons">file_upload</i>Upload to Spotify
</button>
I had two other subscribes, not just one more, as I thought in my previous post. I removed them. Now everything references minsRemaining:
<mat-card style="width:120px;text-align:center;" [style.background-color]="minsRemaining <= 5 ? 'red' : 'transparent'"> <mat-card-content>Session minutes remaining</mat-card-content> <mat-card-content style="font-size:30pt;padding-top:10px;padding-bottom:10px;"> {{minsRemaining}} </mat-card-content> </mat-card>
Survive not only page reloads, but also sleeping
With the above in place, I started working on how to keep it accurate through page reloads. I got something working, but then found that timer stalls when a PC goes to sleep. I’d wake up a sleeping PC and the timer was off by the number of minutes slept.
So I changed to examining time elapsed on the clock, rather than trying to make my process accurately tick each minute. The following code survives both page reloads and sleeping.
I am refreshing every five seconds now. This keeps my precision to within 5 seconds after page reload or sleeping. The 3600 is 60 minutes in seconds:
// sessionStorage survives page reloads if (!sessionStorage.getItem(this.accessToken)) { sessionStorage .setItem(this.accessToken,Date.now().toString()); } //Date.now()-startedAt survives sleeping Observable.timer(0,5000) .map(() => parseInt(sessionStorage.getItem(this.accessToken))) .map(startedAt => 3600-(Date.now()-startedAt)/1000) .map(secsRemaining => Math.ceil(secsRemaining/60)) .takeWhile(minsRemaining => minsRemaining >= 0) .subscribe(x => this.minsRemaining=x);
expires_in=3600
Spotify indicates the number of seconds for the session in the url they return. It’s always been 3600 seconds, which is 60 minutes, but if they ever do return something different, I should honor it. So instead of hardcoding 3600, I parse it out of the url. This is not some sophisticated thing that I could tap into to find out how much time is left. It’s simply how many seconds total the token is good for. It stays the same even when the page is refreshed.
try {this.expiresIn=parseInt( window.location.hash.match(/expires_in=(\d+)/)[1] );} catch (ERR) {this.expiresIn=0} // sessionStorage survives page reloads if (!sessionStorage.getItem(this.accessToken)) { sessionStorage .setItem(this.accessToken,Date.now().toString()); } //Date.now()-startedAt survives sleeping Observable.timer(0,5000) .map(() => parseInt(sessionStorage.getItem(this.accessToken))) .map(startedAt => this.expiresIn-(Date.now()-startedAt)/1000) .map(secsRemaining => Math.ceil(secsRemaining/60)) .takeWhile(minsRemaining => minsRemaining >= 0) .subscribe(x => this.minsRemaining=x);
Session timer problem
Spotify’s web API uses OAuth for user authentication. I am using the option that produces an access token that works for sixty minutes and then expires. I want the user to know about this, so that it is clear what has happened if the session expires. Ordinary use of Shufflizer should not take more than a minute or two, but, of course, users can behave differently from what we expect, including simply leaving the app sitting open for a while and then returning.
My initial solution
I have created an Observable.timer that counts down minutes. I render this in a box. I turn the background red when we are down to five minutes. When we are down to zero then I also disable the upload to spotify button. It isn’t going to work any more.
Problem: Observable.timer is cold
This has been working, but not exactly right. After a bit of testing I have concluded that timer is a cold observable, rather than hot. I am starting two sixty minutes timers, not one. The first timer starts when the page loads. It is reported in the box. This is good. The second timer starts when the user makes an edit to the playlist. It controls the disable of the button. This is bad. Waiting until the user makes an edit is not what I intended and is not accurate.
Here is my observable. This counts down minutes from 60:
this.sessionCountdown = Observable.timer(0, 60000) .map(mins => 60 - mins) .takeWhile(minsRemaining => minsRemaining >= 0);
My problem is in my HTML template, in my TypeScript expression for the disable. TypeScript’s or operator (the double-bar ||) does not execute its second part unless its first part is false. I coded an async pipe subscribe for the second part, not realizing that this subscribe creates its own instance of the timer. The user finally makes an edit to the playlist. Only then does TypeScript flop over to the other side of the double-bar, therefore starting the timer:
<button (click)="uploadToSpotify()"
mat-raised-button
[disabled]="!playlistIsEdited()||(sessionCountdown | async)<=0">
<i class="material-icons">file_upload</i>
Upload to Spotify
</button>
I am going to work on making the timer a singleton, so that I get something more like a hot observable.
While I am working on this, I also need to deal with page reloads. I think I will do that with session storage. The timer should not start over if the page reloads.
Some technical design decisions
Not a Progressive Web App
Shufflizer has no Progressive Web App service workers. There is no off-line scenario. Bad Internet connection? It’s best if you come back later.
No lazy loading
Lazy loading is the idea of incrementally obtaining only the data necessary to render the viewport.
The user is going to reorder the entire playlist. So I need the entire playlist. I don’t lazy load it. This is what Spotify’s native Windows app does when you click on a playlist. I can tell by the way it performs. It loads the whole thing.
I am not downloading songs. The playlist is just the track listing, not the audio itself. Even for a few thousand songs this is not a challenging amount of data for today’s hardware – even a phone.
I do get all of the album art. I did try lazy loading these images but I did not like the results. It made the user experience more sluggish and strange. I prefer the user wait a couple more seconds up front to get a smooth, normal scrolling experience.
No server-side code
Shufflizer is pure Angular/TypeScript. MEAN stack? Nope. It’s just the A.
Having no server-side code makes it easy to comply with this rule in the Spotify Developer Terms of Service:
My only server is my web server, and it is totally stock.
There are some trade-offs for this simplicity. I will talk about them in a future post.