Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance project structure and functionality #380

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
github: TheLastGimbus
ko_fi: thelastgimbus
liberapay: TheLastGimbus
custom: [ "https://www.paypal.me/TheLastGimbus" ]
custom:
- "https://www.paypal.me/TheLastGimbus"
- "https://ko-fi.com/thelastgimbus"
- "https://liberapay.com/TheLastGimbus"
5 changes: 2 additions & 3 deletions .github/workflows/build-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ jobs:
else
echo "Unknown OS: $RUNNER_OS"
exit 69
fi
- uses: dart-lang/setup-dart@v1
- run: dart pub get
- name: Build exe
run: dart compile exe bin/gpth.dart -o ./${{ steps.exe_name.outputs.name }}
- name: Upload apk as artifact
- name: Upload exe as artifact
uses: actions/upload-artifact@v3
with:
name: gpth-nightly-${{ runner.os }}
name: gpth-nightly-${{ matrix.os }}
path: ./${{ steps.exe_name.outputs.name }}
if-no-files-found: error
3 changes: 2 additions & 1 deletion .github/workflows/dart-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ jobs:
- run: dart pub get
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- run: dart analyze --fatal-infos
- name: Run analyzer
run: dart analyze --fatal-infos --fatal-warnings
8 changes: 4 additions & 4 deletions .github/workflows/new-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
run: echo "tag=$(echo ${{ github.ref }} | sed 's/refs\/tags\///')" >> $GITHUB_OUTPUT
- name: Get changelog
run: python scripts/get_changelog.py --version ${{ steps.clean_tag.outputs.tag }} > ./body-file.txt
# Just in case changelogs won't work out
# - name: Get tag message
# id: tag_message
# run: echo "name=message=$(git tag -l --format='%(contents)' ${{ github.ref }})" >> $GITHUB_OUTPUT
# Just in case changelogs won't work out
# - name: Get tag message
# id: tag_message
# run: echo "name=message=$(git tag -l --format='%(contents)' ${{ github.ref }})" >> $GITHUB_OUTPUT
- name: Create GH-Release
uses: softprops/action-gh-release@v1
with:
Expand Down
28 changes: 27 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Ignore IDE directories
.idea/
.vscode/

Expand All @@ -8,8 +9,33 @@
# Conventional directory for build output.
build/

# Ignore photos and output directories
photos/

ALL_PHOTOS/
output/

# Ignore log files
*.log

# Ignore analysis options
analysis_options.yaml

# Ignore generated files
*.g.dart
*.freezed.dart

# Ignore coverage output
coverage/

# Ignore test result files
test-results/

# Ignore temporary files
*.tmp
*.temp

# Ignore macOS specific files
.DS_Store

# Ignore Windows specific files
Thumbs.db
142 changes: 134 additions & 8 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,141 @@

include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.
linter:
rules:
- camel_case_types
- prefer_const_constructors
- prefer_const_literals_to_create_immutables
- avoid_print
- prefer_final_fields
- unnecessary_this
- prefer_single_quotes
- avoid_redundant_argument_values
- prefer_typing_uninitialized_variables
- avoid_empty_else
- avoid_init_to_null
- avoid_returning_null_for_future
- avoid_unnecessary_containers
- avoid_void_async
- always_declare_return_types
- always_specify_types
- annotate_overrides
- avoid_annotating_with_dynamic
- avoid_as
- avoid_catches_without_on_clauses
- avoid_returning_this
- avoid_types_as_parameter_names
- avoid_unused_constructor_parameters
- avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_extensions
- cancel_subscriptions
- close_sinks
- comment_references
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- diagnostic_describe_all_properties
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- file_names
- hash_and_equals
- iterable_contains_unrelated_type
- join_return_with_assignment
- library_names
- library_prefixes
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- non_constant_identifier_names
- null_closures
- omit_local_variable_types
- one_member_abstracts
- only_throw_errors
- overridden_fields
- package_api_docs
- package_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_bool_in_asserts
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_contains
- prefer_double_quotes
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_locals
- prefer_final_parameters
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_initializing_formals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_iterable_whereType
- prefer_mixin
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- secure_pubspec_urls
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- type_annotate_public_apis
- type_init_formals
- unawaited_futures
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unsafe_html
- unrelated_type_equality_checks
- use_function_type_syntax_for_parameters
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_string_buffers
- use_super_parameters
- use_test_throws_matchers
- use_to_and_as_if_applicable
- valid_regexps

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**
analyzer:
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
- '**/generated/**'
- '**/build/**'
- '**/coverage/**'
- '**/test-results/**'

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
Expand Down
41 changes: 38 additions & 3 deletions bin/gpth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import 'package:gpth/media.dart';
import 'package:gpth/moving.dart';
import 'package:gpth/utils.dart';
import 'package:path/path.dart' as p;
import 'package:logging/logging.dart';

final Logger _logger = Logger('GooglePhotosHelper');

const helpText = """GooglePhotosTakeoutHelper v$version - The Dart successor

Expand All @@ -26,6 +29,13 @@ Then, run: gpth --input "folder/with/all/takeouts" --output "your/output/folder"
const barWidth = 40;

void main(List<String> arguments) async {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.message}');
});

setupLogging(); // From lib/logging_setup.dart

final parser = ArgParser()
..addFlag('help', abbr: 'h', negatable: false)
..addOption(
Expand Down Expand Up @@ -157,8 +167,12 @@ void main(List<String> arguments) async {
for (final extractor in dateExtractors) {
date = await extractor(file);
if (date != null) {
await file.setLastModified(date);
set++;
try {
await file.setLastModified(date);
set++;
} catch (e) {
_logger.warning("Failed to set last modified date for ${file.path}: $e");
}
break;
}
}
Expand Down Expand Up @@ -207,6 +221,27 @@ void main(List<String> arguments) async {
}
await output.create(recursive: true);

final parser = ArgParser()
..addOption('input', abbr: 'i', defaultsTo: '.')
..addOption('output', abbr: 'o')
..addFlag('dry-run', abbr: 'd')
..addFlag('deduplicate', help: 'Remove duplicates via content hashing')
..addOption('date-format', defaultsTo: 'yyyy/yyyy-MM');

try {
final results = parser.parse(args);
await processDirectory(
Directory(results['input']),
outputDir: Directory(results['output']),
dryRun: results['dry-run'],
deduplicate: results['deduplicate'],
dateFormat: results['date-format'],
);
} catch (e, stackTrace) {
_logger.severe('Fatal error: $e', stackTrace);
exit(1);
}

/// ##################################################

// Okay, time to explain the structure of things here
Expand Down Expand Up @@ -258,7 +293,7 @@ void main(List<String> arguments) async {

// recursive=true makes it find everything nicely even if user id dumb 😋
await for (final d in input.list(recursive: true).whereType<Directory>()) {
if (isYearFolder(d)) {
if (await isYearFolder(d)) {
yearFolders.add(d);
} else if (await isAlbumFolder(d)) {
albumFolders.add(d);
Expand Down
2 changes: 1 addition & 1 deletion lib/date_extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export 'date_extractors/exif_extractor.dart';
export 'date_extractors/guess_extractor.dart';
export 'date_extractors/json_extractor.dart';

/// Function that can take a file and potentially extract DateTime of it
/// A function type that takes a [File] and potentially extracts a [DateTime] from it.
typedef DateTimeExtractor = Future<DateTime?> Function(File);
34 changes: 31 additions & 3 deletions lib/date_extractors/exif_extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import 'dart:math';
import 'package:exif/exif.dart';
import 'package:gpth/utils.dart';
import 'package:mime/mime.dart';
import 'package:image/image.dart' as img;

DateTime? extractDateFromExif(File file) {
try {
final bytes = file.readAsBytesSync();
final image = img.decodeImage(bytes);
if (image == null) {
_logger.warning('Unsupported image format: ${file.path}');
return null;
}
final exifDate = image.exif?.dateTimeOriginal;
return exifDate ?? file.lastModifiedSync();
} catch (e) {
_logger.warning('EXIF extraction failed for ${file.path}: $e');
return null;
}
}

/// DateTime from exif data *potentially* hidden within a [file]
///
Expand All @@ -19,7 +36,13 @@ Future<DateTime?> exifExtractor(File file) async {
// i have nvme + btrfs, but still, will leave as is
final bytes = await file.readAsBytes();
// this returns empty {} if file doesn't have exif so don't worry
final tags = await readExifFromBytes(bytes);
Map<String, IfdTag> tags;
try {
tags = await readExifFromBytes(bytes);
} catch (e) {
_logger.warning('Failed to read EXIF data from ${file.path}: $e');
return null;
}
String? datetime;
// try if any of these exists
datetime ??= tags['Image DateTime']?.printable;
Expand All @@ -32,10 +55,15 @@ Future<DateTime?> exifExtractor(File file) async {
.replaceAll('/', ':')
.replaceAll('.', ':')
.replaceAll('\\', ':')
.replaceAll(': ', ':0')
.replaceAll(': ', ':0');
if (datetime.length < 19) {
_logger.warning('Invalid EXIF datetime format in ${file.path}: $datetime');
return null;
}
datetime = datetime
.substring(0, min(datetime.length, 19))
.replaceFirst(':', '-') // replace two : year/month to comply with iso
.replaceFirst(':', '-');
// now date is like: "1999-06-23 23:55"
return DateTime.tryParse(datetime);
}
}
Loading