—————————————————————package
File::Sticker::Scribe;
$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
common::sense;
use
File::LibMagic;
=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__