The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

$File::Sticker::Scribe::VERSION = '4.0101';
=head1 NAME
File::Sticker::Scribe - read, write and standardize meta-data from files
=head1 VERSION
version 4.0101
=head1 SYNOPSIS
use File::Sticker::Scribe;
my $scribe = File::Sticker::Scribe->new(%args);
my $meta = $scribe->read_meta($filename);
$scribe->write_meta(%args);
=head1 DESCRIPTION
This will read and write meta-data from files in various formats,
and standardize it to a common nomenclature,
such as "tags" for things called tags, or Keywords or Subject etc.
The standard nomenclature is:
=over
=item url
The source URL of this file (ref 'dublincore.source')
=item creator
The author or artist who created this. (ref 'dublincore.creator')
=item title
The title of the item. (ref 'dublincore.title')
=item description
The description of the item. (ref 'dublincore.description')
=item tags
The item's tags. (ref 'Keywords').
=back
Other fields will be called whatever the user has pre-configured.
=cut
use List::MoreUtils qw(uniq);
=head1 DEBUGGING
=head2 whoami
Used for debugging info
=cut
sub whoami { ( caller(1) )[3] }
=head1 METHODS
=head2 new
Create a new object, setting global values for the object.
my $obj = File::Sticker::Scribe->new();
=cut
sub new {
my $class = shift;
my %parameters = (@_);
my $self = bless ({%parameters}, ref ($class) || $class);
return ($self);
} # new
=head2 init
Initialize the object.
Check if all the required parameters are there.
$scribe->init(wanted_fields=>{title=>'TEXT',count=>'NUMBER',tags=>'MULTI'});
=cut
sub init {
my $self = shift;
my %parameters = @_;
foreach my $key (keys %parameters)
{
$self->{$key} = $parameters{$key};
}
$self->{file_magic} = File::LibMagic->new(follow_symlinks=>1);
# Set the writable fields from the known and readonly fields
if (exists $self->{wanted_fields}
and defined $self->{wanted_fields})
{
my %writable = ();
my $known = $self->known_fields();
my $readonly = $self->readonly_fields();
foreach my $field (keys %{$known})
{
# If it is Known and Not Readonly, it is writable
if (!(exists $readonly->{$field}
and defined $readonly->{$field}))
{
$writable{$field} = $known->{$field};
}
}
$self->{writable_fields} = \%writable;
}
} # init
=head2 name
The name of the scribe; this is basically the last component
of the module name. This works as either a class function or a method.
$name = $self->name();
$name = File::Sticker::Scribe::name($class);
=cut
sub name {
my $class = shift;
my $fullname = (ref ($class) ? ref ($class) : $class);
my @bits = split('::', $fullname);
return pop @bits;
} # name
=head2 priority
The priority of this scribe. Scribes with higher priority
get tried first. This is useful where there may be more
than one possible meta-data format for a file, such as
EXIF versus XATTR.
This works as either a class function or a method.
This must be overridden by the specific scribe class.
$priority = $self->priority();
$priority = File::Sticker::Scribe::priority($class);
=cut
sub priority {
my $class = shift;
return 0;
} # priority
=head2 allow
If this scribe can be used for the given file and the wanted_fields,
then this returns true.
if ($scribe->allow($file))
{
....
}
=cut
sub allow {
my $self = shift;
my $file = shift;
say STDERR whoami(), " file=$file" if $self->{verbose} > 2;
my $okay = $self->allowed_file($file);
if ($okay) # okay so far
{
say STDERR 'Scribe ' . $self->name() . ' allows filetype of ' . $file if $self->{verbose} > 1;
$okay = $self->allowed_fields();
}
return $okay;
} # allow
=head2 allowed_file
If this scribe can be used for the given file, then this returns true.
This must be overridden by the specific scribe class.
if ($scribe->allowed_file($file))
{
....
}
=cut
sub allowed_file {
my $self = shift;
my $file = shift;
return 0;
} # allowed_file
=head2 allowed_fields
If this writer can be used for the known and wanted fields, then this returns true.
By default, if there are no wanted_fields, this returns false.
(But this may be overridden by subclasses)
if ($writer->allowed_fields())
{
....
}
=cut
sub allowed_fields {
my $self = shift;
my $okay = 1;
if (exists $self->{wanted_fields}
and defined $self->{wanted_fields})
{
# the wanted fields must be a subset of the (known fields + readonly fields)
my $known_fields = $self->known_fields();
my $readonly_fields = $self->readonly_fields();
foreach my $fn (keys %{$self->{wanted_fields}})
{
if ((!exists $known_fields->{$fn}
or !defined $known_fields->{$fn}
or !$known_fields->{$fn})
and (!exists $readonly_fields->{$fn}
or !defined $readonly_fields->{$fn}
or !$readonly_fields->{$fn}))
{
$okay = 0;
last;
}
}
}
else
{
say STDERR 'Scribe ' . $self->name() . ' was not given wanted_fields' if $self->{verbose} > 1;
$okay = 0;
}
return $okay;
} # allowed_fields
=head2 known_fields
Returns the fields which this scribe knows about.
This must be overridden by the specific scribe class.
my $known_fields = $scribe->known_fields();
=cut
sub known_fields {
my $self = shift;
return undef;
} # known_fields
=head2 readonly_fields
Returns the fields which this scribe knows about, which can't be overwritten,
but are allowed to be "wanted" fields. Things like file-size etc.
my $readonly_fields = $scribe->readonly_fields();
=cut
sub readonly_fields {
my $self = shift;
return {filesize=>'NUMBER'};
} # readonly_fields
=head2 writable_fields
Returns the fields which this scribe knows about, which can be written into.
my $writable_fields = $scribe->writable_fields();
=cut
sub writable_fields {
my $self = shift;
return $self->{writable_fields};
} # writable_fields
=head2 read_meta
Read the meta-data from the given file.
This must be overridden by the specific scribe class.
my $meta = $scribe->read_meta($filename);
=cut
sub read_meta {
my $self = shift;
my $filename = shift;
} # read_meta
=head2 add_field_to_file
Adds a field to a file, taking account of whether it is a multi-value field or not.
This requires the old meta-data for the file to be passed in.
$scribe->add_field_to_file(filename=>$filename,
field=>$field,
value=>$value,
old_meta=>\%meta);
=cut
sub add_field_to_file {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $field = $args{field};
my $value = $args{value};
my $old_meta = $args{old_meta};
my $type = (
exists $self->{wanted_fields}->{$field}
and defined $self->{wanted_fields}->{$field}
? $self->{wanted_fields}->{$field}
: 'UNKNOWN'
);
say STDERR "field=$field value=$value type=$type" if $self->{verbose} > 2;
if ($type =~ /multi/i)
{
return $self->update_multival_field(
filename=>$filename,
field=>$field,
value=>$value,
old_vals=>$old_meta->{$field});
}
else
{
$self->replace_one_field(
filename=>$filename,
field=>$field,
value=>$value);
}
} # add_field_to_file
=head2 delete_field_from_file
Completely remove the given field.
For multi-value fields, it removes ALL the values.
This must be overridden by the specific scribe class.
$scribe->delete_field_from_file(filename=>$filename,field=>$field);
=cut
sub delete_field_from_file {
my $self = shift;
my %args = @_;
my $filename = $args{filename};
my $field = $args{field};
} # delete_field_from_file
=head2 replace_all_meta
Overwrite the existing meta-data with that given.
$scribe->replace_all_meta(filename=>$filename,meta=>\%meta);
=cut
sub replace_all_meta {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $meta = $args{meta};
# overwrite the known writable fields
# ignore the unknown fields
my $writable = $self->writable_fields();
foreach my $field (sort keys %{$writable})
{
if (exists $meta->{$field}
and defined $meta->{$field})
{
$self->replace_one_field(filename=>$filename,
field=>$field,
value=>$meta->{$field});
}
else # not there, remove it
{
$self->delete_field_from_file(filename=>$filename,field=>$field);
}
}
} # replace_all_meta
=head1 Helper Functions
Private interface.
=head2 update_multival_field
A multi-valued field could have individual values added or removed from it.
This expects a comma-separated list of individual values, prefixed with an operation:
'+' or nothing -- add the values
'-' -- remove the values
'=' -- replace the values
This also needs to know the existing values of the multi-valued field.
The old values are either a reference to an array, or a string with comma-separated values.
$scribe->update_multival_field(filename=>$filename,
field=>$field_name,
value=>$value,
old_vals=>$old_vals);
=cut
sub update_multival_field {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $field = $args{field};
my $value = $args{value};
my $old_vals = $args{old_vals};
my $prefix = '+';
if ($value =~ /^([+=-])(.*)/)
{
$prefix = $1;
$value = $2;
}
say STDERR "prefix='$prefix'" if $self->{verbose} > 2;
if ($prefix eq '=')
{
$self->replace_one_field(
filename=>$filename,
field=>$field,
value=>$value);
}
else
{
if ($prefix eq '-')
{
$self->delete_multival_from_file(
filename=>$filename,
field=>$field,
value=>$value,
old_vals=>$old_vals);
}
else
{
$self->add_multival_to_file(
filename=>$filename,
field=>$field,
value=>$value,
old_vals=>$old_vals);
}
}
} # update_multival_field
=head2 add_multival_to_file
Add a multi-valued field to the file.
Needs to know the existing values of the multi-valued field.
The old values are either a reference to an array, or a string with comma-separated values.
$scribe->add_multival_to_file(filename=>$filename,
field=>$field_name,
value=>$value,
old_vals=>$old_vals);
=cut
sub add_multival_to_file {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $fname = $args{field};
my $old_vals = $args{old_vals};
# allow for multiple values, comma-separated
my @vals = ($args{value});
if ($args{value} =~ /,/)
{
@vals = split(/,/, $args{value});
}
# add new value(s) to existing taglike-values
my @old_values = ();
if (ref $old_vals eq 'ARRAY')
{
@old_values = @{$old_vals};
}
elsif (!ref $old_vals)
{
@old_values = split(/,/, $old_vals);
}
my @newvals = @old_values;
push @newvals, @vals;
@newvals = uniq @newvals;
my $newvals = join(',', @newvals);
$self->replace_one_field(filename=>$filename,
field=>$fname,
value=>$newvals);
} # add_multival_to_file
=head2 delete_multival_from_file
Remove one value of a multi-valued field.
Needs to know the existing values of the multi-valued field.
The old values are either a reference to an array, or a string with comma-separated values.
$scribe->delete_multival_from_file(filename=>$filename,
value=>$value,
field=>$field_name,
old_vals=>$old_vals);
=cut
sub delete_multival_from_file ($%) {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $fname = $args{field};
my $old_vals = $args{old_vals};
# allow for multiple values, comma-separated
my @vals = ($args{value});
if ($args{value} =~ /,/)
{
@vals = split(/,/, $args{value});
}
my %to_delete = ();
foreach my $t (@vals)
{
$to_delete{$t} = 1;
}
# remove value from existing values
# preserving the existing order
my @old_values = ();
if (ref $old_vals eq 'ARRAY')
{
@old_values = @{$old_vals};
}
elsif (!ref $old_vals)
{
@old_values = split(/,/, $old_vals);
}
my @newvals = ();
foreach my $t (@old_values)
{
if (! exists $to_delete{$t})
{
push @newvals, $t;
}
}
my $newvals = join(',', @newvals);
$self->replace_one_field(filename=>$filename,
field=>$fname,
value=>$newvals);
} # delete_multival_from_file
=head2 replace_one_field
Overwrite the given field. This does no checking.
This must be overridden by the specific scribe class.
$scribe->replace_one_field(filename=>$filename,field=>$field,value=>$value);
=cut
sub replace_one_field {
my $self = shift;
my %args = @_;
my $filename = $args{filename};
my $field = $args{field};
my $value = $args{value};
} # replace_one_field
=head1 BUGS
Please report any bugs or feature requests to the author.
=cut
1; # End of File::Sticker::Scribe
__END__