awful itunes hack for album listening

It’s bugged me that iTunes makes it hard to listen to things as albums. Sure, it has shuffle-by-album, but smart playlists are all per-track.

After years of meaning to, this morning I wrote a (very very slow) Mac::Glue script to build a playlist of unrated or highly-rated albums that I haven’t listened to lately. When I was nearly done and looked into one little bug, I found some other similar scripts. Oh well!

I’ll eventually update this to avoid having it pick all albums by one artist, but for now, it’s good. Thanks to it, I am re-listening to Method Man’s Tical.

#!/usr/bin/perl
use strict;
use warnings;
use Mac::Glue qw(:glue);
use List::Util qw(sum);

my $itunes = Mac::Glue->new('iTunes');

my $pl = $itunes->obj(
  playlist => whose(name => equals => 'Regular Music')
)->get;

my $albumen = $itunes->obj(
  playlist => whose(name => equals => 'Albumen')
)->get;

die "no albumen" unless $albumen;

{
  my $tracks = $itunes->obj(
    'track' => gAll,
    playlist => $albumen->prop('index')->get,
  );

  for my $t ( $tracks->get ){
    $t->delete;
  }
}

print "getting tracks\n";
my @tracks = $pl->obj('tracks')->get;

my %album;

while (my $track = shift @tracks) {
  my $trackid = $track->prop('database ID')->get;
  my $album   = $track->prop('album')->get;
  my $artist  = $track->prop('compilation')->get
              ? '-'
              : $track->prop('artist')->get;

  next unless defined $album and defined $artist;
  next unless length  $album and length  $artist;

  my $rec = $album{ $album, $artist } ||= [];

  printf "storing record of $trackid ($album/$artist); %s remain\n",
    scalar @tracks;

  push @$rec, {
    id     => $trackid,
    rating => scalar $track->prop('rating')->get,
    played => scalar $track->prop('played date')->get, # epoch sec
    size   => scalar $track->prop('size')->get, # in bytes
  };
}

my $DEFAULT_TIME = time - 30 * 86_400;
my %avg_age;

ALBUM: for my $key (keys %album) {
  my ($album, $artist) = split $;, $key;
  printf "considering (%s/%s)\n", $album, $artist;

  my @tracks = @{ $album{ $key } };

  unless (@tracks > 4) {
    printf "skipping (%s/%s); too few tracks\n", $album, $artist;
    delete $album{$key};
    next ALBUM;
  }

  my @lp_dates = map { undef $_ if $_ eq 'msng'; $_ || $DEFAULT_TIME }
                 map { $_->{played} }
                 @tracks;

  my $avg_age  = time - (sum(@lp_dates) / @lp_dates);
  $avg_age{ $key } = $avg_age;

  if ($avg_age < 86_400 * 30) {
    printf "skipping (%s/%s); too recent\n", $album, $artist;
    delete $album{$key};
    next ALBUM;
  }

  my @ratings    = grep { $_ > 0 } map { $_->{rating} } @tracks;
  my $avg_rating = sum(@ratings) / @ratings if @ratings;

  if ($avg_rating and $avg_rating < 60) {
    printf "skipping (%s/%s); too lousy\n", $album, $artist;
    delete $album{$key};
    next ALBUM;
  }

  printf "keeping (%s/%s) @ %s\n", $album, $artist, $avg_rating || '(n/a)';
}

my $total_size = 0;
ADDITION: for my $key (sort { $avg_age{$b} <=> $avg_age{$a} } keys %album) {
  my @tracks = @{ $album{ $key } };

  for my $track (@tracks) {
    $total_size += $track->{size};

    my $t = $itunes->obj(
      track => whose('database id' => equals => $track->{id})
    )->get;
    
    $itunes->duplicate($t, to => $albumen);
  }

  last ADDITION if $total_size > 500_000_000;
}
Written on November 26, 2008
🏷 itunes
🐫 perl
🧑🏽‍💻 programming