Example #1
0
def test_get_date_from_exif_subseconds():
    assert Date().from_exif({
        "CreateDate": "2017-01-01 01:01:01.20"
    }) == {
        "date": datetime(2017, 1, 1, 1, 1, 1),
        "subseconds": "20"
    }
Example #2
0
def test_get_date_from_exif_colon():
    assert Date().from_exif({
        "CreateDate": "2017:01:01 01:01:01"
    }) == {
        "date": datetime(2017, 1, 1, 1, 1, 1),
        "subseconds": ""
    }
Example #3
0
def test_get_date_from_exif_invalid():
    assert Date().from_exif({
        "CreateDate": "Invalid"
    }) == {
        "date": None,
        "subseconds": ""
    }
Example #4
0
def test_get_date_custom_regex_invalid():
    """
    A valid regex with a matching filename.
    Return none because there is not enough information in the filename.
    """
    date_regex = re.compile(r"(?P<hour>\d{2})\.(?P<minute>\d{2})\.(?P<second>\d{2})")
    assert Date("19.20.00.jpg").from_exif({}, False, date_regex) is None
Example #5
0
def test_get_date_from_custom_date_field():
    assert Date().from_exif({
        "CustomField": "2017:01:01 01:01:01"
    }, date_field="CustomField") == {
        "date": datetime(2017, 1, 1, 1, 1, 1),
        "subseconds": ""
    }
Example #6
0
def test_get_date_custom_regex_no_match():
    """
    A valid regex with a non-matching filename.
    """
    date_regex = re.compile(
        r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})[_-]?(?P<hour>\d{2})\.(?P<minute>\d{2})\.(?P<second>\d{2})"
    )  # noqa: E501
    assert Date("Foo.jpg").from_exif({}, False, date_regex) is None
Example #7
0
def test_get_date_custom_regex():
    """
    A valid regex with a matching filename. Returns a datetime.
    """
    date_regex = re.compile(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})[_-]?(?P<hour>\d{2})\.(?P<minute>\d{2})\.(?P<second>\d{2})")
    assert Date("IMG_27.01.2015-19.20.00.jpg").from_exif({}, False, date_regex) == {
        "date": datetime(2015, 1, 27, 19, 20, 00),
        "subseconds": ""
    }
Example #8
0
def test_get_date_custom_regex_optional_time():
    """
    A valid regex with a matching filename that doesn't have hour information.
    However, the regex in question has hour information as optional.
    """
    date_regex = re.compile(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})[_-]?((?P<hour>\d{2})\.(?P<minute>\d{2})\.(?P<second>\d{2}))?")
    assert Date("IMG_27.01.2015.jpg").from_exif({}, False, date_regex) == {
        "date": datetime(2015, 1, 27, 0, 0, 00),
        "subseconds": ""
    }
Example #9
0
def main(argv):
    check_dependencies()

    move = False
    link = False
    date_regex = None
    dir_format = os.path.sep.join(['%Y', '%m', '%d'])

    try:
        opts, args = getopt.getopt(argv[2:], "d:r:mlh", ["date=", "regex=", "move", "link", "help"])
    except getopt.GetoptError:
        help(version)
        sys.exit(2)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            help(version)
            sys.exit(2)

        if opt in ("-d", "--date"):
            if not arg:
                printer.print.error("Date format cannot be empty")
            dir_format = Date().parse(arg)

        if opt in ("-m", "--move"):
            move = True
            printer.line("Using move strategy!")

        if opt in ("-l", "--link"):
            link = True
            printer.line("Using link strategy!")

        if opt in ("-r", "--regex"):
            try:
                date_regex = re.compile(arg)
            except:
                printer.error("Provided regex is invalid!")
                sys.exit(2)

    if link and move:
        printer.error("Can't use move and link strategy together!")
        sys.exit(1)

    if len(argv) < 2:
        help(version)
        sys.exit(2)

    return Phockup(
        argv[0], argv[1],
        dir_format=dir_format,
        move=move,
        link=link,
        date_regex=date_regex
    )
Example #10
0
    def get_file_name_and_path(self, file):
        """
        Returns target file name and path
        """
        exif_data = Exif(file).data()
        if exif_data and self.is_image_or_video(exif_data['MIMEType']):
            date = Date(file).from_exif(exif_data, self.date_regex)
            output = self.get_output_dir(date)
            target_file_name = self.get_file_name(file, date).lower()
            target_file_path = os.path.sep.join([output, target_file_name])
        else:
            output = self.get_output_dir(False)
            target_file_name = os.path.basename(file)
            target_file_path = os.path.sep.join([output, target_file_name])

        return output, target_file_name, target_file_path
Example #11
0
    def get_file_name_and_path(self, filename):
        """
        Returns target file name and path
        """
        exif_data = Exif(filename).data()
        target_file_type = None

        if exif_data and 'MIMEType' in exif_data:
            target_file_type = self.get_file_type(exif_data['MIMEType'])

        if target_file_type in ['image', 'video']:
            date = Date(filename).from_exif(exif_data, self.timestamp,
                                            self.date_regex, self.date_field)
            output = self.get_output_dir(date)
            target_file_name = self.get_file_name(filename, date)
            if not self.original_filenames:
                target_file_name = target_file_name.lower()
            target_file_path = os.path.sep.join([output, target_file_name])
        else:
            output = self.get_output_dir(False)
            target_file_name = os.path.basename(filename)
            target_file_path = os.path.sep.join([output, target_file_name])

        return output, target_file_name, target_file_path, target_file_type
Example #12
0
def parse_args(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(
        description=PROGRAM_DESCRIPTION,
        formatter_class=argparse.RawTextHelpFormatter)

    parser.version = f"v{__version__}"

    parser.add_argument(
        '-v',
        '--version',
        action='version',
    )

    parser.add_argument(
        '-d',
        '--date',
        action='store',
        type=Date().parse,
        help="""\
Specify date format for OUTPUTDIR directories.

You can choose different year format (e.g. 17 instead of 2017) or decide to skip the
day directories and have all photos sorted in year/month.

Supported formats:
    YYYY - 2016, 2017 ...
    YY   - 16, 17 ...
    MM   - 07, 08, 09 ...
    M    - July, August, September ...
    m    - Jul, Aug, Sept ...
    DD   - 27, 28, 29 ... (day of month)
    DDD  - 123, 158, 365 ... (day of year)

Example:
    YYYY/MM/DD -> 2011/07/17
    YYYY/M/DD  -> 2011/July/17
    YYYY/m/DD  -> 2011/Jul/17
    YY/m-DD    -> 11/Jul-17
""",
    )

    exclusive_group_link_move = parser.add_mutually_exclusive_group()

    exclusive_group_link_move.add_argument(
        '-m',
        '--move',
        action='store_true',
        help="""\
Instead of copying the process will move all files from the INPUTDIR to the OUTPUTDIR.
This is useful when working with a big collection of files and the remaining free space
is not enough to make a copy of the INPUTDIR.
""",
    )

    exclusive_group_link_move.add_argument(
        '-l',
        '--link',
        action='store_true',
        help="""\
Instead of copying the process will make hard links to all files in INPUTDIR and place
them in the OUTPUTDIR.
This is useful when working with working structure and want to create YYYY/MM/DD
structure to point to same files.
""",
    )

    parser.add_argument(
        '-o',
        '--original-names',
        action='store_true',
        help="""\
Organize the files in selected format or using the default year/month/day format but
keep original filenames.
""",
    )

    parser.add_argument(
        '-t',
        '--timestamp',
        action='store_true',
        help="""\
Use the timestamp of the file (last modified date) if there is no EXIF date information.
If the user supplies a regex, it will be used if it finds a match in the filename.
This option is intended as "last resort" since the file modified date may not be
accurate, nevertheless it can be useful if no other date information can be obtained.
""",
    )

    parser.add_argument(
        '-y',
        '--dry-run',
        action='store_true',
        help="""\
Does a trial run with no permanent changes to the filesystem.
So it will not move any files, just shows which changes would be done.
""",
    )

    parser.add_argument(
        '--maxdepth',
        type=int,
        default=-1,
        choices=range(0, 255),
        metavar='1-255',
        help="""\
Descend at most 'maxdepth' levels (a non-negative integer) of directories
""",
    )

    parser.add_argument(
        '-r',
        '--regex',
        action='store',
        type=re.compile,
        help="""\
Specify date format for date extraction from filenames if there is no EXIF date
information.

Example:
    {regex}
    can be used to extract the date from file names like the following
    IMG_27.01.2015-19.20.00.jpg.
""",
    )

    parser.add_argument(
        '-f',
        '--date-field',
        action='store',
        help="""\
Use a custom date extracted from the exif field specified.
To set multiple fields to try in order until finding a valid date, use spaces to
separate fields inside a string.

Example:
    DateTimeOriginal
    "DateTimeOriginal CreateDate FileModifyDate"

These fields are checked by default when this argument is not set:
    "SubSecCreateDate SubSecDateTimeOriginal CreateDate DateTimeOriginal"

To get all date fields available for a file, do:
    exiftool -time:all -mimetype -j <file>
""",
    )

    exclusive_group_debug_silent = parser.add_mutually_exclusive_group()

    exclusive_group_debug_silent.add_argument(
        '--debug',
        action='store_true',
        default=False,
        help="""\
Enable debugging.
""",
    )

    exclusive_group_debug_silent.add_argument(
        '--quiet',
        action='store_true',
        default=False,
        help="""\
Run without output.
""",
    )

    parser.add_argument(
        '--log',
        action='store',
        help="""\
Specify the output directory where your log file should be exported.
This flag can be used in conjunction with the flag `-q | --quiet`.
""",
    )

    parser.add_argument(
        'input_dir',
        metavar='INPUTDIR',
        help="""\
Specify the source directory where your photos are located.
""",
    )

    parser.add_argument(
        'output_dir',
        metavar='OUTPUTDIR',
        help="""\
Specify the output directory where your photos should be exported.
""",
    )

    parser.add_argument(
        '--file-type',
        type=str,
        choices=['image', 'video'],
        metavar='image|video',
        help="""\
By default, Phockup addresses both image and video files.
If you want to restrict your command to either images or
videos only, use `--file-type=[image|video]`.
""",
    )

    return parser.parse_args(args)
Example #13
0
def main(argv):
    check_dependencies()

    move = False
    link = False
    date_regex = None
    dir_format = os.path.sep.join(['%Y', '%m', '%d'])
    original_filenames = False
    timestamp = False
    date_field = None
    dry_run = False

    try:
        opts, args = getopt.getopt(argv[2:], "d:r:f:mltoyh", ["date=", "regex=", "move", "link", "original-names", "timestamp", "date-field=", "dry-run", "help"])
    except getopt.GetoptError:
        help(version)
        sys.exit(2)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            help(version)
            sys.exit(2)

        if opt in ("-d", "--date"):
            if not arg:
                printer.error("Date format cannot be empty")
            dir_format = Date().parse(arg)

        if opt in ("-m", "--move"):
            move = True
            printer.line("Using move strategy")

        if opt in ("-l", "--link"):
            link = True
            printer.line("Using link strategy")

        if opt in ("-o", "--original-names"):
            original_filenames = True
            printer.line("Using original filenames")

        if opt in ("-r", "--regex"):
            try:
                date_regex = re.compile(arg)
            except:
                printer.error("Provided regex is invalid")

        if opt in ("-t", "--timestamp"):
            timestamp = True
            printer.line("Using file's timestamp")

        if opt in ("-y", "--dry-run"):
            dry_run = True
            printer.line("Dry run only, not moving files only showing changes")

        if opt in ("-f", "--date-field"):
            if not arg:
                printer.error("Date field cannot be empty")
            date_field = arg
            printer.line("Using as date field: %s" % date_field)


    if link and move:
        printer.error("Can't use move and link strategy together")

    if len(argv) < 2:
        help(version)
        sys.exit(2)

    return Phockup(
        argv[0], argv[1],
        dir_format=dir_format,
        move=move,
        link=link,
        date_regex=date_regex,
        original_filenames=original_filenames,
        timestamp=timestamp,
        date_field=date_field,
        dry_run=dry_run,
    )
Example #14
0
def test_get_date_none_on_no_error():
    assert Date("IMG_2017_01.jpg").from_exif({}) is None
Example #15
0
def test_get_date_none_on_no_info():
    assert Date("Foo.jpg").from_exif({}) is None
Example #16
0
def test_get_date_filename_invalid():
    assert Date("IMG_20170101_999999.jpg").from_exif({}) is None
Example #17
0
def test_get_date_from_filename():
    assert Date("IMG_20170101_010101.jpg").from_exif({}) == {
        "date": datetime(2017, 1, 1, 1, 1, 1),
        "subseconds": ""
    }
Example #18
0
def test_get_date_from_exif_strip_timezone_sub_sec():
    assert Date().from_exif(
        {"SubSecCreateDate": "2019:10:06 11:02:50.575+01:00"}) == {
            "date": datetime(2019, 10, 6, 11, 2, 50),
            "subseconds": "575"
        }
Example #19
0
def main():
    check_dependencies()

    parser = argparse.ArgumentParser(
        description=PROGRAM_DESCRIPTION,
        formatter_class=argparse.RawTextHelpFormatter,
    )
    parser.version = "v%s" % __version__

    parser.add_argument(
        "-v",
        "--version",
        action="version",
    )

    parser.add_argument(
        "-d",
        "--date",
        action="store",
        type=Date().parse,
        help="""Specify date format for OUTPUTDIR directories.
You can choose different year format (e.g. 17 instead of 2017) or decide to
skip the day directories and have all photos sorted in year/month.

Supported formats:
    YYYY - 2016, 2017 ...
    YY   - 16, 17 ...
    MM   - 07, 08, 09 ...
    M    - July, August, September ...
    m    - Jul, Aug, Sept ...
    DD   - 27, 28, 29 ... (day of month)
    DDD  - 123, 158, 365 ... (day of year)

Example:
    YYYY/MM/DD -> 2011/07/17
    YYYY/M/DD  -> 2011/July/17
    YYYY/m/DD  -> 2011/Jul/17
    YY/m-DD    -> 11/Jul-17
        """,
    )

    exclusive_group = parser.add_mutually_exclusive_group()

    exclusive_group.add_argument(
        "-m",
        "--move",
        action="store_true",
        help=
        """Instead of copying the process will move all files from the INPUTDIR to the OUTPUTDIR.
This is useful when working with a big collection of files and the
remaining free space is not enough to make a copy of the INPUTDIR.
        """,
    )

    exclusive_group.add_argument(
        "-l",
        "--link",
        action="store_true",
        help=
        """Instead of copying the process will make hard links to all files in INPUTDIR and place them in the OUTPUTDIR.
This is useful when working with working structure and want to create YYYY/MM/DD structure to point to same files.
        """,
    )

    parser.add_argument(
        "-o",
        "--original-names",
        action="store_true",
        help=
        "Organize the files in selected format or using the default year/month/day format but keep original filenames.",
    )

    parser.add_argument(
        "-t",
        "--timestamp",
        action="store_true",
        help=
        """Use the timestamp of the file (last modified date) if there is no EXIF date information.
If the user supplies a regex, it will be used if it finds a match in the filename.
This option is intended as "last resort" since the file modified date may not be accurate,
nevertheless it can be useful if no other date information can be obtained.
        """,
    )

    parser.add_argument(
        "-y",
        "--dry-run",
        action="store_true",
        help="Don't move any files, just show which changes would be done.",
    )

    parser.add_argument(
        "--maxdepth",
        type=int,
        default=-1,
        choices=range(0, 255),
        metavar="1-255",
        help=
        "Descend at most 'maxdepth' levels (a non-negative integer) of directories",
    )

    parser.add_argument(
        "-r",
        "--regex",
        action="store",
        type=re.compile,
        help=
        """Specify date format for date extraction from filenames if there is no EXIF date information.

Example:
    {regex}
    can be used to extract the date from file names like the following IMG_27.01.2015-19.20.00.jpg.
        """,
    )

    parser.add_argument(
        "-f",
        "--date-field",
        action="store",
        help="""Use a custom date extracted from the exif field specified.
To set multiple fields to try in order until finding a valid date,
use spaces to separate fields inside a string.

Example:
    DateTimeOriginal
    "DateTimeOriginal CreateDate FileModifyDate"

These fields are checked by default when this argument is not set:
    "SubSecCreateDate SubSecDateTimeOriginal CreateDate DateTimeOriginal"

To get all date fields available for a file, do:
    exiftool -time:all -mimetype -j <file>
        """,
    )

    parser.add_argument(
        "-q",
        "--quiet",
        action="store_true",
        help="""Run without output
        """,
    )

    parser.add_argument(
        "input_dir",
        metavar="INPUTDIR",
        help="Specify the source directory where your photos are located.",
    )
    parser.add_argument(
        "output_dir",
        metavar="OUTPUTDIR",
        help=
        "Specify the output directory where your photos should be exported.",
    )

    args = parser.parse_args()

    return Phockup(args.input_dir,
                   args.output_dir,
                   dir_format=args.date,
                   move=args.move,
                   link=args.link,
                   date_regex=args.regex,
                   original_filenames=args.original_names,
                   timestamp=args.timestamp,
                   date_field=args.date_field,
                   dry_run=args.dry_run,
                   quiet=args.quiet,
                   max_depth=args.maxdepth)
Example #20
0
def test_parse_date_format_valid():
    """Test that parse_date_format returns a valid format for strftime"""
    datetime.strftime(
        datetime.now(),
        Date().parse("YYYY YY m MM M DDD DD \\ /")
    )