Managing ID3 tags in MP3 files manually is never fun, so I made a Perl script to automate it as much as possible. This script tries to extract information from the path and filename of music files. Additionally, it consults my SQL database of liner notes information and includes any information stored there.
The main loop iterates across identified targets (or the current directory if none are specified), descending into directories recursively. For each file, a series of regular expressions are tested to determine if artist, album title, or track name/number information is in the filename in any commonly seen formats. If the artist and album can be identified, these are normalized for a database query to see if the album record exists. Such album records include a full tracklist, the year, record label, and catalogue numbers. Once all available information has been extracted, the results are presented on STDOUT and stored in the file's ID3 tag.
The script responds to several command-line options. In
particular, the -u option causes information
found to be stored in the ID3 tag. Without this option, no
changes are made. The other options mainly concern output control,
including a mode to output id3v2
commands for
manual use.
#!/usr/bin/perl # 2011-03-05 evool # 2011-03-08 more... # 2011-04-13 found it again! moving to audio.admin. # 2011-04-14 it works. now to start applying it. slowly. with debugging # 2011-11-26 adapted as tag_from_name from tagger # 2011-12-08 merged improved tag_from_name filedetection & id3 setter # functions back into tagger, with added 'smooth' # 2012-08-21 fixed node db reader, improved path reader to catch more patterns, # including multidisc sets, added track/title mode # 2013-02-15 logic error - cannot retrieve matches from regex after subsequent # substitution; improved case 2 # 2013-03-24 upgraded node db reader to read year/label/cat.no/barcode # 2013-04-11 connected year from node to tag writer # 2013-04-15 changed "no leading dot" policy to "no parents" to better reflect # the anti-loop behaviour i want without impeding the ability to hit # any file # 2013-05-07 - added strip & decode for all non-title columns in musicbase.node # body lines. # - "no leading dot" policy revision exposed filename parser to # malfunction, choosing title.00.mp3 over artist.title.00.mp3 # - workaround: don't call tagger against '.' -- use '*' # instead (same diff, eh?) [ed. fixed on 6/14] # - converted per-line info conveyance from title-only to title, # time, bpm, note (any other appended columns' contents) # 2013-06-15 - fixed capitalist to work properly, accomodating in-word # apostrophes and non-space word delineations. # - fixed 'this' path interference: when '/./' is detected, it's # replaced with '/'. this fixes 5/7 workaround, above. # - added 'list' mode in which nothing is changed, but a summary for # each track is displayed showing existing tag contents, and what # we've computed it should be # - made '.' the default path # - changed default operation to 'list' # - converted -d dryrun into -u update. nothing changed without -u # 2014-02-01 - reformatted long lines. tested on insekt.dreamscape use strict; use DBI; use MP3::Tag; use Getopt::Std; use Cwd 'abs_path'; my $update = 0; # don't touch a damn thing unless this is set my $verbose = 0; # talk too much my $display_list = 1; # display information and change nothing my $display_command = 0; # output a id3v2 command for every update my $titletrackmode = 0; our $opt_u; our $opt_v; our $opt_l; our $opt_h; our $opt_c; our $opt_t; getopts('cuhltv'); print "opth:$opt_h\nargv:".scalar(@ARGV)."\n"; # h takes precedence and precludes any other operation if ($opt_h) { print <connect('DBI:mysql:musicbase:localhost:3306','astro','redacted'); if ($dbh) { $sth = $dbh->prepare("SELECT * FROM node WHERE base=? AND name=?"); } else { print "WARNING could not connect db\n" if ($verbose); } my %nodecache = (); # store nodes here to skip reload for each track foreach my $file (@list) { if ($file !~ /^\//) { $file = abs_path().'/'.$file; } $file =~ s/\/\.\//\//g; print "---------------------------------------------------------\n"; print "reading file '$file'\n" if ($verbose); # load id3 from file my $id3tag = MP3::Tag->new($file); $id3tag->get_tags(); my $tag; if (exists $id3tag->{'ID3v2'}) { $tag = $id3tag->{'ID3v2'}; } elsif (exists $id3tag->{'ID3v1'}) { $tag = $id3tag->{'ID3v1'}; } my %id3 = (); # copy fields of interest into a descriptive hash if (defined $tag) { $id3{'artist'} = $tag->artist; if ($id3{'artist'} =~ /No Artist/i) { undef $id3{'artist'}; } $id3{'album'} = $tag->album; $id3{'title'} = $tag->title; $id3{'tracknum'} = $tag->track; $id3{'year'} = $tag->year; if ($id3{'title'} =~ /Track\s*(\d+)$/i) { if ($id3{'tracknum'} eq '') { $id3{'tracknum'} = $1; } undef $id3{'title'}; } if ($verbose) { printf( " found ID3 tag: artist [%s] album [%s] year [%s] title [%s] track [%d]\n", $id3{'artist'}, $id3{'album'}, $id3{'year'}, $id3{'title'}, $id3{'tracknum'} ); } } my $matched = 0; # matches */audio/artist_name/album_name/Artist Name - Song Title.mp3 if ($file =~ /^(.*)\/(MUSIC|audio)\/([^\/]+)\/([^\/]+)\/(.+)\.([-a-zA-Z0-9]{3,4})/i) { if ($verbose) { print " in valid path\n"; } my $stump = $1; my $musicdir = $2; my $nodekey = 0; $id3{'disc'} = 1; $id3{'artist'} = $3; $id3{'album'} = $4; my $trackfile = $5; my $ext = $6; # matches must FOLLOW all assignments from $n $id3{'artist'} =~ s/_/ /g; $id3{'album'} =~ s/_/ /g; $id3{'year'} = 0; if ($id3{'album'} =~ /(.+)_(19\d\d|20\d\d)$/) { $id3{'album'} = $1; $id3{'year'} = $2; } if ($verbose) { printf( " matched stump [%s] musicdir [%s] artist [%s] album [%s] file [%s] ext [%s]\n", $stump, $musicdir, $id3{'artist'}, $id3{'album'}, $trackfile, $ext ); } # matches */audio/artist_name/album_name/Artist Name - Song Title.mp3 if ($trackfile =~ /^([^\.]+) -{1,2} ([^\.]+)$/) { $id3{'artist'} = $1; $id3{'artist'} =~ s/_/ /g; $id3{'title'} = $2; $id3{'title'} =~ s/_/ /g; $matched = 1; # matches */audio/artist_name/album_name/01.Song Title.mp3 } elsif ($trackfile =~ /^(track)?(\d{1,2})\.(.*)$/i) { $id3{'tracknum'} = $2; $id3{'title'} = $3; if ($id3{'title'} =~ /^([^\.]+)\.([^\.]+)$/) { $id3{'artist'} = $1; $id3{'title'} = $2; } $id3{'title'} =~ s/_/ /g; $id3{'artist'} =~ s/^(\w+),(\w+)$/$2 $1/; # name flipped around $nodekey = $id3{'artist'}. print "tracknum: $id3{'tracknum'}\n"; $matched = 2; # the following cases check musicbase.node for a title # matches */audio/artist_name/album_name/artist.album.disc_1.01.song_title.mp3 } elsif ($trackfile =~ /^([^\.]+)\.([^\.]+)\.disc_?(\d{1,2})\.(\d{2,3})(\.(.+))?$/) { $id3{'artist'} = $1; $nodekey = $id3{'artist'}; $id3{'album'} = $2; $id3{'disc'} = $3; $id3{'tracknum'} = $4; if ($5 ne '') { $id3{'title'} = $5; $id3{'title'} =~ s/_/ /g; } $id3{'artist'} =~ s/_/ /g; $id3{'artist'} =~ s/^(\w+), (\w+)$/$2 $1/; $id3{'album'} =~ s/_/ /g; $matched = 3; # matches */audio/artist_name/album_name/artist.album.01.song_title.mp3 } elsif ($trackfile =~ /^([^\.]+)\.([^\.]+)\.(\d{2,3})(\.(.+))?$/) { $id3{'artist'} = $1; $nodekey = $id3{'artist'}; $id3{'album'} = $2; $id3{'tracknum'} = $3; if ($5 ne '') { $id3{'title'} = $5; $id3{'title'} =~ s/_/ /g; } $id3{'artist'} =~ s/_/ /g; $id3{'artist'} =~ s/^(\w+), (\w+)$/$2 $1/; $id3{'album'} =~ s/_/ /g; $matched = 4; # matches */audio/artist_name/album_name/somethingirrelevant.01.song_title.mp3 } elsif ($trackfile =~ /^.+\.([^\.]+)\.(\d{2,3})(\.(.+))?$/) { $nodekey = $id3{'artist'}; $id3{'tracknum'} = $1; if ($2 ne '') { $id3{'title'} = $2; $id3{'title'} =~ s/_/ /g; } # here we assume artist and album were populated by id3 or other tag $id3{'artist'} =~ s/^(\w+), (\w+)$/$2 $1/; $matched = 5; } if ($nodekey) { $nodekey =~ s/_/ /g; getAlbum($id3{'artist'},$id3{'album'}); $nodekey =~ s/^(\w+,)(\w+)$/$1 $2/; # don't flip - need order for db check $nodekey .= $id3{'album'}; if ($verbose) { printf( " nodekey:[%s] disc:%s track:%d year:%s\n", $nodekey, $id3{'disc'}, $id3{'tracknum'}, $nodecache{$nodekey}->[$id3{'disc'}][0]{'year'} ); } if ($nodecache{$nodekey} && ($nodecache{$nodekey}->[$id3{'disc'}][$id3{'tracknum'}])) { $id3{'title'} = $nodecache{$nodekey}->[$id3{'disc'}][$id3{'tracknum'}]{'title'}; $id3{'time'} = $nodecache{$nodekey}->[$id3{'disc'}][$id3{'tracknum'}]{'time'}; $id3{'bpm'} = $nodecache{$nodekey}->[$id3{'disc'}][$id3{'tracknum'}]{'bpm'}; $id3{'note'} = $nodecache{$nodekey}->[$id3{'disc'}][$id3{'tracknum'}]{'note'}; } if ($nodecache{$nodekey} && ($nodecache{$nodekey}->[$id3{'disc'}][0])) { $id3{'year'} = $nodecache{$nodekey}->[$id3{'disc'}][0]{'year'}; $id3{'label'} = $nodecache{$nodekey}->[$id3{'disc'}][0]{'label'}; if ($nodecache{$nodekey}->[$id3{'disc'}][0]{'catalog'} ne '') { $id3{'note'} = 'catalog number: '.$nodecache{$nodekey}->[$id3{'disc'}][0]{'catalog'}.' '.$id3{'note'}; } } } } else { if ($verbose) { print " path not recognized in filename [$file]\n"; } } if ($matched) { $id3{'artist'} =~ s/^(\w+), ?(\w+)$/$2 $1/; capitalist(\$id3{'artist'}); capitalist(\$id3{'album'}); capitalist(\$id3{'title'}); capitalist(\$id3{'label'}); $id3{'tracknum'} = sprintf('%d',$id3{'tracknum'}); if ($verbose) { printf( " matched pattern %s: artist:[%s] album:[%s] tracknum:[%d] title:[%s] year [%s] time [%s] bpm [%s] label [%s]\n", $matched, $id3{'artist'}, $id3{'album'}, $id3{'tracknum'}, $id3{'title'}, $id3{'year'}, $id3{'time'}, $id3{'bpm'}, $id3{'label'} ); } if ($update) { if (exists $id3tag->{'ID3v2'}) { $id3tag->{'ID3v2'}->remove_tag; } $id3tag->new_tag('ID3v2'); if ($id3{'tracknum'} > 0) { $id3tag->{'ID3v2'}->add_frame('TRCK',$id3{'tracknum'}); } $id3tag->{'ID3v2'}->add_frame('TIT2',$id3{'title'}); if (! $titletrackmode) { $id3tag->{'ID3v2'}->add_frame('TPE1',$id3{'artist'}); $id3tag->{'ID3v2'}->add_frame('TALB',$id3{'album'}); if ($id3{'year'} > 0) { $id3tag->{'ID3v2'}->add_frame('TYER',$id3{'year'}); } if ($id3{'time'} != 0) { $id3tag->{'ID3v2'}->add_frame('TLEN',$id3{'time'}); } if ($id3{'bpm'} > 0) { $id3tag->{'ID3v2'}->add_frame('TBPM',$id3{'bpm'}); } if ($id3{'label'} ne '') { $id3tag->{'ID3v2'}->add_frame('TPUB',$id3{'label'}); } if ($id3{'note'} ne '') { $id3tag->{'ID3v2'}->add_frame('TCOMM',$id3{'note'}); } } $id3tag->{'ID3v2'}->write_tag; if (exists $id3tag->{'ID3v1'}) { $id3tag->{'ID3v1'}->remove_tag; } print "updated tag for $file\n"; } if ($display_list) { my $exist_artist = get_frame($tag,'TPE1'); my $exist_album = get_frame($tag,'TALB'); my $exist_track = get_frame($tag,'TRCK'); my $exist_title = get_frame($tag,'TIT2'); my $exist_year = get_frame($tag,'TYER'); my $exist_length = get_frame($tag,'TLEN'); my $exist_bpm = get_frame($tag,'TBPM'); my $exist_label = get_frame($tag,'TPUB'); printf( "=========\nfile %s\n artist -[%s] +[%s]\n album -[%s] +[%s]\n tracknum -[%s] +[%s]\n ". "title -[%s] +[%s]\n year -[%s] +[%s]\n time -[%s] +[%s]\n bpm -[%s] +[%s]\n label -[%s] +[%s]\n---------\n", $file, $exist_artist, $id3{'artist'}, $exist_album, $id3{'album'}, $exist_track, $id3{'tracknum'}, $exist_title, $id3{'title'}, $exist_year, $id3{'year'}, $exist_length, $id3{'time'}, $exist_bpm, $id3{'bpm'}, $exist_label, $id3{'label'} ); } if ($display_command) { my $cmd = 'id3v2'; $cmd .= ' -s'; # does anything even use v1? $cmd .= " -t \"$id3{'title'}\""; $cmd .= ($id3{'tracknum'} > 0)?" -T $id3{'tracknum'}":''; if (! $titletrackmode) { $cmd .= " -a \"$id3{'artist'}\""; $cmd .= " -A \"$id3{'album'}\""; $cmd .= ($id3{'year'} > 0)?" -y $id3{'year'}":''; } $cmd .= " \"$file\"\n"; print $cmd; } } } sub get_frame($$) { my ($tag,$name) = @_; if (defined $tag) { my @frame = $tag->get_frame($name); return shift @frame; } return ''; } # end main code # this does NOT capitalize the first word in a parenthesis, # or in any case where the beginning of the "word" is a symbol. sub capitalist($) { my $debug = 0; my $words = shift @_; my @outwords = (); while ($$words =~ /^(\W*)([\w\'\d]+)(.*)$/) { $$words = $3; if ($debug) { print "word: $2"; } push(@outwords,$1.ucfirst($2)); } $$words = join('',@outwords).$$words; $$words =~ s/"/\\"/g; } # retrieves an album from database sub getAlbum($$) { my $debug = 1; my ($artist,$album) = @_; $artist =~ s/^([-\.\w\d][-\.\w\d\s]+),([-\.\w\d][-\.\w\d\s]+)$/$1, $2/; if ($debug && $verbose) { print " getAlbum: init $artist.$album\n"; } if ($sth && !$nodecache{$artist.$album}) { if ($debug && $verbose) { print " getAlbum: 'astro.info.inv.cd.lib.$artist','$album' not cached\n"; } $sth->execute('astro.info.inv.cd.lib.'.$artist,$album); # we rashly assume only one record will match if (my $node = $sth->fetchrow_hashref()) { if ($debug && $verbose) { print " getAlbum: got node for $artist.$album\n"; } my $disc = 1; my $cachetmp = []; # loop through tracks described in node foreach my $line (split("\n",$node->{'body'})) { if ($line =~ /^\s*disc\s*(\d{1,2})\s*$/) { # correct disc! $disc = sprintf('%d',$1); if ($debug && $verbose) { print " set disc: $disc\n"; } # matches any properly formatted line except when time is followed # by artist on a compilation } elsif ($line =~ /^\s*(\d{2,3})\.\s+(.*)$/) { if ( ! defined($cachetmp->[$disc])) { $cachetmp->[$disc] = (); } my $name = $2; my $num = sprintf('%d',$1); my $bpm = 0; my $time = 0; my $note = ''; # strip and decode appended columns while ($name =~ /^(.+\S)\s{2,}(.*)$/) { $name = $1; my $col = $2; if ($col =~ /^\d{1,2}[.:]\d{2}$/) { $time = $col; $time =~ s/\./\:/; } elsif ($col =~ /^\d{3}$/) { $bpm = $col; } else { $note .= $col; } } $cachetmp->[$disc][$num]{'title'} = $name; $cachetmp->[$disc][$num]{'time'} = $time; $cachetmp->[$disc][$num]{'bpm'} = $bpm; $cachetmp->[$disc][$num]{'note'} = $note; if ($debug && $verbose) { printf( " set track\n disc: %s\n track:%s\n name:%s\n time:%s\n bpm:%s\n note:[%s]\n", $disc, $num, $name, $time, $bpm, $note ); } } elsif ($line =~ /^(nodate|\d{4}) (.+) (.+) \|\| (---|\d+ \d+)$/) { # eg: 1982 sire CD23737 || 75992 37372 if ($1 ne 'nodate') { $cachetmp->[$disc][0]{'year'} = $1; } $cachetmp->[$disc][0]{'label'} = $2; if ($3 ne '---') { $cachetmp->[$disc][0]{'catalog'} = $3; } if ($4 ne '---') { $cachetmp->[$disc][0]{'barcode'} = $4; } if ($debug && $verbose) { printf( " set data\n year: %s\n label:%s\n catalog:%s\n barcode:%s\n", $cachetmp->[$disc][0]{'year'}, $cachetmp->[$disc][0]{'label'}, $cachetmp->[$disc][0]{'catalog'}, $cachetmp->[$disc][0]{'barcode'} ); } } } $nodecache{$artist.$album} = $cachetmp; return $node; # don't need this anymore, but what the hell } else { if ($debug && $verbose) { print " getAlbum: no results\n"; } } } return 0; } # adds to a list of files with the target or the target's contents sub buildList($$) { my ($listref,$target) = @_; if ($target =~ /\.\./) { # anti-skip print "avoiding parent folders. too many loops\nuse an absolute path.\n"; } else { # contains no parent reference $target =~ s/\/$//; if (-f $target) { push(@$listref,$target); } elsif (-d $target) { if (opendir(my $dh,$target)) { my @dirlist = (); while (my $entry = readdir($dh)) { if ($entry !~ /^\./) { push(@dirlist,$entry); } } foreach my $entry (sort @dirlist) { buildList($listref,$target.'/'.$entry); } } } else { print "i don't do files like '$target'\n"; } } } # EOF
At the top of this page there is a small multicoloured clock which represents my age in days, hours, minutes, and seconds in the America/Vancouver time zone. This clock is actually an SVG image that is generated dynamically in Javascript and updated each second.
The numerals are a custom font designed to represent numbers in a 5-segment digital glyph. The use of the custom font identifies the number as my datestamp, to avoid confusion with other numbers or written dates.
The script below has functions to generate the datestamp (adtime), generate custom SVG glyphs (Comp and astruneGlyph), and compose the final SVG document representing the datestamp (displayAstruneTime).
function adtime(showSeconds) { showSeconds = showSeconds || false; Today = new Date(); raw = Date.parse(Today.toGMTString())/1000; spm = 60; sph = 60*spm; spd = 24*sph; raw -= spm*Today.getTimezoneOffset(); // compensate for time zone day = Math.floor(raw/spd); raw -= day*spd; hour = Math.floor(raw/sph); if (hour<10) { hour = '0'+String(hour); } raw -= Number(hour)*sph; min = Math.floor(raw/spm); if (min<10) { min = '0'+String(min); } if (showSeconds) { sec = String(Math.floor(raw%spm)); if (sec<10) { sec = '0'+String(sec); } min += sec; } return (String(day+154)+String(hour)+String(min)); } function newSVGElement(svgElement) { var svgNS = "http://www.w3.org/2000/svg"; el = document.createElementNS(svgNS,svgElement); return el; } function Comp() { this.glyph = newSVGElement('path'); this.glyph.setAttribute('fill','#FF0000'); this.glyph.setAttribute('stroke','none'); this.glyph.setAttribute('stroke-width','10'); this.path = ''; }; Comp.prototype.dn = function () { this.path += 'q 55,-20,110,0 '; this.path += 'l 0,-400 '; }; Comp.prototype.dr = function () { this.path += 'q 420,400,980,600 40,-12,80,0 -600,-240,-940,-600 '; this.path += 'l 0,-400 '; }; Comp.prototype.dl = function () { this.path += 'q -340,360,-940,600 40,-12,80,0 560,-200,980,-600 '; this.path += 'l 0,-400 '; }; Comp.prototype.an = function () { this.path += 'q -55,20,-110,0 '; this.path += 'z '; }; Comp.prototype.ar = function () { this.path += 'q 340,-360,940,-600 -40, 12,-80,0 -560, 200,-980, 600 '; this.path += 'z '; }; Comp.prototype.al = function () { this.path += 'q -420,-400,-980,-600 -40,12,-80,0 600,240,940,600 '; this.path += 'z '; }; Comp.prototype.nu = function () { this.path += 'q 55,-20,110,0 '; this.path += 'l 0,-145 '; this.path += 'l 145,0 '; this.path += 'q -20,-55,0,-110 '; this.path += 'l -145,0 '; this.path += 'l 0,-145 '; this.path += 'q -55,20,-110,0 '; this.path += 'l 0,145 '; this.path += 'l -145,0 '; this.path += 'q 20,55,0,110 '; this.path += 'l 145,0 '; this.path += 'z '; }; // a little ovoid at centre stave Comp.prototype.dot = function () { this.path += 'm 0,-200 c 0,50 100,50 100,0 c 0,-50 -100,-50 -100,0'; this.path += 'z '; }; function astruneGlyph(glyphChar,x_pos) { var arGlyph = new Comp(); arGlyph.path += 'M '+x_pos+',1000 '; switch (glyphChar) { case '0' : arGlyph.nu(); break; case '1' : arGlyph.dn(); arGlyph.an(); break; case '2' : arGlyph.dr(); arGlyph.al(); break; case '3' : arGlyph.dl(); arGlyph.al(); break; case '4' : arGlyph.dn(); arGlyph.ar(); break; case '5' : arGlyph.dl(); arGlyph.ar(); break; case '6' : arGlyph.dr(); arGlyph.an(); break; case '7' : arGlyph.dn(); arGlyph.al(); break; case '8' : arGlyph.dr(); arGlyph.ar(); break; case '9' : arGlyph.dl(); arGlyph.an(); break; case '.' : arGlyph.dot(); break; }; arGlyph.glyph.setAttribute('d',arGlyph.path); return arGlyph.glyph; } function displayAstruneTime(divid,showSeconds) { showSeconds = showSeconds || false; // first remove existing AstruneTimeSvg node, if it exists if (existing = document.getElementById('AstruneTimeSvg')) { existing.parentNode.removeChild(existing); } // rebuild AstruneTimeSvg node var kern = 300; var left = 980 - (showSeconds ? (kern * 1.5) : 0); var div = document.getElementById(divid); var arSvg = newSVGElement('svg'); arSvg.setAttribute("version", "1.2"); arSvg.setAttribute("baseProfile", "tiny"); arSvg.setAttribute("viewBox", "0 0 4360 1600"); arSvg.id = 'AstruneTimeSvg'; var ad = adtime(showSeconds); pad = 0; for (i = 0; i < ad.length; i++) { if (i == 5) { arSvg.appendChild(astruneGlyph('.',(((i + pad) * kern) + left))); pad++; } if (i == 9) { arSvg.appendChild(astruneGlyph('.',(((i + pad) * kern) + left))); pad++; } arSvg.appendChild(astruneGlyph(ad.substr(i,1),(((i + pad) * kern) + left))); } div.appendChild(arSvg); }
This python script resides on our household linux laptops to detect various external devices and automatically update configurations, eg. screen resolutions/arrangement. In this form we run it manually, but it's just a few steps away from being adapted for autonomous operation.
#!/usr/bin/python # 157631811 v2.py astro@starshade.org # toggles to correct external screen combination based on what xrandr thinks is going on # 159660026 added conversion of ins to R-CTL for merope only. # 162432212 made monitor detection iterative/homogenous, so HDMI and VGA are both accommodated # 162472143 select default resolution (+) instead of largest supported, since that's not necessarily native. # # to-do # add printer bt 00:16:38:C1:ED:19 # hidd --connect # bluetooth://001638C1ED19 import re import socket import subprocess from commands import * # dict of filesystem devices we manage filedevices_managed = { 'atlas':'679515bc-6a15-46a9-80d4-3fe08c8aba9a', 'maia':'a83b9dcc-fa71-441e-a1ca-759c7701aa30', 'transcend16':'7BBB-9007', } # --------- filesystem device management --------------- # list of disk uuids filedevices_attached = [] status, filedevices_udevadm = getstatusoutput('udevadm info --export-db') for disk_by_uuid_match in re.findall(r"S: disk/by-uuid/(.*)",filedevices_udevadm): filedevices_attached.append(disk_by_uuid_match) # full text of mount command output status, filedevices_mounted = getstatusoutput('mount') # loop through managed filedevices for device_name,device_uuid in filedevices_managed.items(): # check if device is mounted mountdevpattern = "/dev/.* on /%s type " % ( device_name ) device_mounted = re.search(mountdevpattern,filedevices_mounted) if filedevices_attached.count(device_uuid) > 0: if device_mounted is None: # if attached device is not listed in mount print "detected attached ummounted device %s" % ( device_name ) command = "mount /%s" % ( device_name ) subprocess.call(command, shell=True); # mount it else: # report mounted print "detected attached mounted device %s" % ( device_name ) elif device_mounted is not None: # report problem print "WARNING: detected unattached mounted device %s" % ( device_name ) # -------- input device management ------------ # full text of lsusb command output status, usbdevices_attached = getstatusoutput('lsusb') # correct the macally keyboard alt/menu mapping def xmodmap(xmodmap): command = 'xmodmap -display ":0.0" -e "%s"' % ( xmodmap ) subprocess.call(command, shell=True); xmodmap("remove mod1 = Super_L") xmodmap("remove mod1 = Super_R") xmodmap("remove mod1 = Alt_L") xmodmap("remove mod1 = Alt_R") xmodmap("remove mod4 = Super_L") xmodmap("remove mod4 = Super_R") xmodmap("remove mod4 = Alt_L") xmodmap("remove mod4 = Alt_R") if re.search('2222:0013 MacAlly',usbdevices_attached): print "loading macally keyboard xmodmap" xmodmap("keycode 64 = Super_L NoSymbol Super_L") xmodmap("keycode 108 = Super_R NoSymbol Super_R") xmodmap("keycode 133 = Alt_L Meta_L Alt_L Meta_L") xmodmap("keycode 134 = Alt_R Meta_R Alt_R Meta_R") if socket.gethostname() == 'merope': print "merope: convert ins -> ins" xmodmap("remove Control = Control_L") xmodmap("remove Control = Control_R") xmodmap("keycode 105 = Control_R NoSymbol Control_R") xmodmap("keycode 118 = Insert NoSymbol Insert") xmodmap("add Control = Control_L") xmodmap("add Control = Control_R") else: # macally not connected print "loading stock keyboard xmodmap" xmodmap("keycode 64 = Alt_L Meta_L Alt_L Meta_L") xmodmap("keycode 108 = Alt_R Meta_R Alt_R Meta_R") xmodmap("keycode 133 = Super_L NoSymbol Super_L") xmodmap("keycode 134 = Super_R NoSymbol Super_R") if socket.gethostname() == 'merope': print "merope: convert ins -> rctl" xmodmap("remove Control = Control_L") xmodmap("remove Control = Control_R") xmodmap("keycode 118 = Control_R NoSymbol Control_R") xmodmap("add Control = Control_L") xmodmap("add Control = Control_R") xmodmap("add mod1 = Alt_L") xmodmap("add mod1 = Alt_R") xmodmap("add mod4 = Super_L") # if nostromo n50 is present, load pystromo config # this may require /etc/udev/rules.d/52-pystromo.rules # first get the pid, if any, of running pystromo pystromo_pid = 0 status, pystromoprocs = getstatusoutput('ps aux | grep "python /home/malachi/bin/pystromo/pystromo-remap.py -m" | grep -v grep') if len(pystromoprocs) > 0: for pystromoprocline in re.split("\n",pystromoprocs): pystromoprocarr = re.split(' +',pystromoprocline) pystromo_pid = pystromoprocarr[1] if re.search('050d:0805 Belkin Components Nostromo N50 GamePad',usbdevices_attached): if pystromo_pid == 0: print "loading pystromo config for nostromo n50" command = "~/bin/pystromo/pystromo-remap.py -m ~/bin/pystromo/config/n50_malachi_1.map &" subprocess.call(command, shell=True); elif pystromo_pid != 0: print "killing pystromo" command = "kill -9 %s" % ( pystromo_pid ) subprocess.call(command, shell=True); # ------------ display management ------------- # start with no additional display commands command = 'xrandr' # store details about all detected displays display = {} # always keep primary display anchored at bottom left display['LVDS1'] = { 'res_x':0, 'res_y':0, 'pos_x':0, 'pos_y':0, } # retrieve current configuration status, xrandr = getstatusoutput('xrandr') displays = re.findall(r"([A-Z0-9]+) (disconnected|connected [^\n]*\n(\s+\d+x\d+(\s+\d+\.\d[\s\*\+]*)+\n)+)",xrandr) # work through displays, configuring each appropriately # nb: this only works for ONE external display. attach 2 and it won't work! if len(displays) > 0: for display_lines in displays: display_name = display_lines[0] if display_lines[1] == 'disconnected': print "deconfiguring monitor %s" % (display_name) display[display_name] = { 'connected': False, } offcommand = "xrandr --output %s --off" % (display_name) subprocess.call(offcommand, shell=True) else: print "configuring monitor %s" % (display_name) display[display_name] = { 'connected': True, 'res_x':0, 'res_y':0, 'pos_x':0, 'pos_y':0, } display_modes = display_lines[1] for mode_line in display_modes.split('\n'): mode_params = re.search(r"^\s+(\d+)x(\d+)\s+\d+\.\d([ \*])([ \+])",mode_line) if mode_params: if mode_params.group(4) == '+': display[display_name]['res_x'] = int(mode_params.group(1)) display[display_name]['res_y'] = int(mode_params.group(2)) for display_name in display: if display[display_name]['connected']: ext_pos_x = 0 if display_name != 'LVDS1': ext_pos_x = display['LVDS1']['res_x'] command = "%s --output %s --mode %dx%d --pos %dx%d" % ( command, display_name, display[display_name]['res_x'], display[display_name]['res_y'], ext_pos_x, (display['LVDS1']['res_y'] - display[display_name]['res_y']) ) print 'command: "%s"' % ( command ) subprocess.call(command, shell=True) # EOF
This program, developed as part of an AI course at university, plays 3-dimensional tic-tac-toe against a human opponent in a text console. Despite the relative parsimony of this very simple AI, and the limited set of heuristics that it employs, you can't beat it. We tried.
#include#include "stdio.h" #include "stdlib.h" #include "time.h" const int size = 4; class Matrix { public: Matrix(); void Play(); private: char win; char field[size][size][size]; int sequence[64]; int move; char first; int phaseX; int phaseO; int stkount; int strike[64]; void Clean(); void Show(); char InputYN(); int Category(int x, int y, int z, int & xc, int & yc, int & zc); void VectorListUnidir(int & count, int list[], int x, int y, int z, int dx, int dy, int dz); void VectorList(int & count, int list[], int x, int y, int z, int dx, int dy, int dz); int VectorSet(int list[], int x, int y, int z); void Unpack(const int pack, int & x, int & y, int & z); int Pack(const int x, const int y, const int z); bool Critical(char player, int & phase, int count, int list[], int & mx, int & my, int & mz); int Weight(char player, const int & count, int list[], const int & mx, const int & my, const int & mz); void Unstrike(int x, int y, int z); void Midgame(const char player, int mcount, int mlist[]); void Endgame(const char player); void AIMove(char player); void UserMove(char player); bool Move(const char player, int x, int y, int z); bool CheckStale(); void CheckWin(char player, int element[3]); char CheckBound(char player, int & count, int chk[3], int element[3]); void Error(int message); }; Matrix::Matrix() { Clean(); } void Matrix::Clean() { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { for (int k = 0; k < size; k++) { field[i][j][k] = '.'; } } } move = 0; phaseX = 0; phaseO = 0; stkount = 0; win = ' '; } char Matrix::InputYN() { char player; while ((player != 'X') && (player != 'O')) { cin >> player; if ((player == 'y') || (player == 'Y')) { player = 'X'; } else if ((player == 'n') || (player == 'N')) { player = 'O'; } } return player; } void Matrix::Play() { char replay; int mode; char player; bool stale; do { stale = false; mode = -1; system("clear"); cout << "1: play head-to-head" << endl; cout << "2: play the AI" << endl; cout << "3: kick Chris & Anna's AI's ass." << endl; while ((mode < 0) || (mode > 3)) { cout << "Select play mode [1-3]: "; cin >> mode; mode--; } if (mode == 0) { player = 'X'; } else { if (mode == 1) { cout << "Would you like to go first? [y/n] "; } else { cout << "Would you like the foreign AI to go first? [y/n] "; } player = InputYN(); } while ((win == ' ') && (!stale)) { if (mode == 0) { UserMove(player); } else if ((mode == 1) && (player == 'X')) { UserMove(player); } else if (player == 'O') { AIMove(player); //call our AI move interface } else // (mode == 2) && (player == 'X') { AIMove(player); //call foreign AI move interface } if (player == 'X') { player = 'O'; } else { player = 'X'; } stale = CheckStale(); } Clean(); cout << endl << "Would you like to play again? [y/n] "; cin >> replay; cout << endl; } while ((replay != 'n') && (replay != 'N')); } ////// ***** AIMove cluster int Matrix::Category(int x, int y, int z, int & xc, int & yc, int & zc) { if (x <= 1) {xc = 0;} else {xc = 3;} if (y <= 1) {yc = 0;} else {yc = 3;} if (z <= 1) {zc = 0;} else {zc = 3;} return ((abs(xc-x)) + (abs(yc-y)) + (abs(zc-z))); } void Matrix::VectorListUnidir(int & count, int list[], int x, int y, int z, int dx, int dy, int dz) { bool in = true; while (in) //apply operation chk to position { x += dx; y += dy; z += dz; if ((x >= 0) && (x < size) && (y >= 0) && (y < size) && (z >= 0) && (z < size)) { list[count] = Pack(x,y,z); count++; } else { in = false; } } } void Matrix::VectorList(int & count, int list[], int x, int y, int z, int dx, int dy, int dz) { VectorListUnidir(count,list,x,y,z,dx,dy,dz); dx *= -1; dy *= -1; dz *= -1; VectorListUnidir(count,list,x,y,z,dx,dy,dz); } int Matrix::VectorSet(int list[], int x, int y, int z) // returns # of vectors in list { int xc,yc,zc,dx,dy,dz; int count = 0; VectorList(count, list, x, y, z, 0, 0, 1); VectorList(count, list, x, y, z, 0, 1, 0); VectorList(count, list, x, y, z, 1, 0, 0); int delta = Category(x,y,z,xc,yc,zc); if (delta == 1) //edge { dx = 1; dy = 1; dz = 1; if (xc != x) {dx = 0;} else if (yc != y) {dy = 0;} else {dz = 0;} VectorList(count, list, x, y, z, dx, dy, dz); } else if (delta == 2) //face { dx = xc - x; dy = yc - y; dz = zc - z; VectorList(count, list, x, y, z, dx, dy, dz); } else //delta == 0 or 3 corner or core { if (xc == 3) {dx = -1;} else {dx = 1;} if (yc == 3) {dy = -1;} else {dy = 1;} if (zc == 3) {dz = -1;} else {dz = 1;} VectorList(count, list, x,y,z,0,dy,dz); VectorList(count, list, x,y,z,dx,0,dz); VectorList(count, list, x,y,z,dx,dy,0); VectorList(count, list, x,y,z,dx,dy,dz); } return count; } void Matrix::Unpack(const int pack, int & x, int & y, int & z) { z = pack/16; y = (pack-(z*16))/4; x = pack-((z*16)+(y*4)); } int Matrix::Pack(const int x, const int y, const int z) { return (x + (4*y) + (16*z)); } bool Matrix::Critical(char player, int & phase, int count, int list[], int & mx, int & my, int & mz) { bool triple = false; int t,x,y,z,lx,ly,lz; for (int i = 0;((i < count) && (!triple)); i += 3) { t = 3; for (int j = i; j < (i+3); j++) { Unpack(list[j],x,y,z); if (field[x][y][z] == player) t *= 3; else if (field[x][y][z] == '.') { t *= 1; lx = x; ly = y; lz = z; } else t *= 2; } if (t == 27) { triple = true; mx = lx; my = ly; mz = lz; phase = 1; } } return triple; } int Matrix::Weight(char player, const int & count, int list[], const int & mx, const int & my, const int & mz) { int total = 0; int weight,x,y,z; for (int i = 0; i < count; i += 3) { weight = 1; if (field[mx][my][mz] == player) weight *= 2; else if (field[mx][my][mz] == '.') weight *= 1; else weight *= 3; for (int j = i; j < (i+3); j++) { Unpack(list[j],x,y,z); if (field[x][y][z] == player) weight *= 2; else if (field[x][y][z] == '.') weight *= 1; else weight *= 3; } if (weight%6 == 0) weight = 0; total += weight; } return total; } void Matrix::Unstrike(int x, int y, int z) { bool done = false; int t = Pack(x,y,z); for (int i = 0; ((i < stkount) && (!done)); i++) { if (strike[i] == t) { strike[i] = strike[stkount-1]; done = true; } } if (done) stkount--; } void Matrix::Midgame(const char player, int mcount, int mlist[]) { int weight,bcount,mx,my,mz,bx,by,bz; int best = 0; int blist[21]; for (int i=0; i < mcount; i++) { Unpack(mlist[i],mx,my,mz); bcount = VectorSet(blist,mx,my,mz); weight = Weight(player, bcount, blist, mx, my, mz); if ((weight >= best) && (field[mx][my][mz] == '.')) { bx = mx; by = my; bz = mz; best = weight; } } cout << "AI: midgame (" << best << ") - move to " << bx << ' ' << by << ' ' << bz << endl << endl; if (Move(player,bx,by,bz)) Endgame(player); } void Matrix::Endgame(const char player) { int slist[21]; int t,x,y,z,bx,by,bz,scount; int phase; int best = 0; if (stkount == 0) { for (x = 0; x < 4; x++) for (y = 0; y < 4; y++) for (z = 0; z < 4; z++) if (field[x][y][z] == '.') strike[stkount++] = Pack(x,y,z); } for (int i = 0; i < stkount; i++) { Unpack(strike[i],x,y,z); scount = VectorSet(slist,x,y,z); t = Weight(player,scount,slist,x,y,z); if ((t >= best) && (field[x][y][z] == '.')) { best = t; bx = x; by = y; bz = z; } } cout << "AI: endgame (" << best << ") - move to " << bx << ' ' << by << ' ' << bz << endl << endl; if (player == 'X') phase = phaseX; else phase = phaseO; if (phase == 2) Unstrike(bx,by,bz); Move(player,bx,by,bz); if ((phase == 1) || (phase == 0)) { stkount = 0; phase = 0; } if (player == 'X') phaseX = phase; else phaseO = phase; } void Matrix::AIMove(char player) { int weight,mcount,ocount,t,xc,yc,zc,mx,my,mz,ox,oy,oz; bool done = false; char other; int phase; int mlist[21]; int olist[21]; if (player == 'X') phase = phaseX; else phase = phaseO; t = sequence[move-1]; Unpack(t,ox,oy,oz); if (move == 0) //opening { srandom(time(NULL)); mx = (random()%2)*3; my = (random()%2)*3; mz = (random()%2)*3; cout << "AI: opening 1 - move to " << mx << ' ' << my << ' ' << mz << endl << endl; Move(player,mx,my,mz); } else if (move == 1) //opening { t = Category(ox,oy,oz,xc,yc,zc); if ((t == 0) || (t == 3)) { if (ox == 0) {mx = 3;} else {mx = 0;} //choose opposite corner if (oy == 0) {my = 3;} else {my = 0;} if (oz == 0) {mz = 3;} else {mz = 0;} } else { mx = xc; //choose same corner my = yc; mz = zc; } cout << "AI: opening 2 - move to " << mx << ' ' << my << ' ' << mz << endl << endl; Move(player,mx,my,mz); } else //regular season play { if (player == 'X') {other = 'O';} else {other = 'X';} t = sequence[move-2]; Unpack(t,mx,my,mz); mcount = VectorSet(mlist, mx, my, mz); done = Critical(player, phase, mcount, mlist, mx, my, mz); if (done) { cout << "AI: critical win - move to " << mx << ' ' << my << ' ' << mz << endl << endl; Move(player, mx, my, mz); } else { ocount = VectorSet(olist, ox, oy, oz); done = Critical(other, phase, ocount, olist, ox, oy, oz); if (done) { cout << "AI: crisis prevention - move to " << ox << ' ' << oy << ' ' << oz << endl << endl; Move(player, ox, oy, oz); } else { if (phase == 0) Midgame(player, mcount, mlist); else { if (phase == 2) Unstrike(ox,oy,oz); Endgame(player); } } } } if (player == 'X') phaseX = phase; else phaseO = phase; } void Matrix::UserMove(char player) { int x,y,z; bool invalid = true; do { x = y = z = 0; cout << "Player " << player << endl << " enter x:"; cin >> x; cout << " enter y:"; cin >> y; cout << " enter z:"; cin >> z; if ((x >= 0) && (x < size) && (y >= 0) && (y < size) && (z >= 0) && (z < size)) { invalid = Move(player,x,y,z); } else { Error(1); invalid = true; } } while (invalid); } bool Matrix::Move(const char player, int x, int y, int z) { bool failure = false; int element[3] = {x,y,z}; if (field[element[0]][element[1]][element[2]] != '.') { Error(2); failure = true; } if (!failure) { sequence[move] = Pack(x,y,z); move++; if (move > 40) { if (player == 'X') phaseX = 2; else phaseO = 2; } field[element[0]][element[1]][element[2]] = player; Show(); CheckWin(player, element); failure = false; } return failure; } bool Matrix::CheckStale() { bool stale = true; if (move < 64) { stale = false; } return stale; } void Matrix::CheckWin(char player, int element[3]) { int sphere[13][3] = { {0,0,1},{0,1,0},{1,0,0}, {0,1,1},{0,1,-1},{1,0,1},{1,0,-1},{1,1,0},{1,-1,0}, {1,1,1},{1,1,-1},{1,-1,1},{-1,1,1} }; int vec[3]; int veclen; for (int i = 0; ((i < 13) && (win ==' ')); i++) { veclen = 1; win = CheckBound(player,veclen,sphere[i],element); if (win == ' ') { for (int j=0; j<3; j++) { vec[j] = sphere[i][j] * -1; } win = CheckBound(player,veclen,vec,element); } if (win != ' ') { cout << "Player " << win << " wins." << endl; } } } char Matrix::CheckBound(char player, int & count, int chk[3], int element[3]) { char result = ' '; int check[3] = {element[0],element[1],element[2]}; bool in = true; while (in) //apply operation chk to position { for (int j=0; j<3; j++) { check[j] += chk[j]; } if ((check[0] >= 0) && (check[0] < size) && (check[1] >= 0) && (check[1] < size) && (check[2] >= 0) && (check[2] < size)) { if (field[check[0]][check[1]][check[2]] == player ) { count++; } } else { in = false; } } if (count == 4) { result = player; } return result; } void Matrix::Show() { //system("clear"); for (int i=0; i < size; i++) { for (int j=0; j < size; j++) { for (int k=0; k < size; k++) { cout << field[k][i][j]; } cout << ' '; } cout << endl; } cout << endl; } void Matrix::Error(int message) { if (message == 1) { cout << "Move is out of bounds." << endl << endl; } else if (message == 2) { cout << "Space already Taken." << endl << endl; } else if (message == 3) { cout << "Hey, not your turn!" << endl << endl; } } main() { Matrix Test; Test.Play(); }