Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db1947dd1e | ||
|
|
248fdbcf02 | ||
|
|
71cb01572d | ||
|
|
51b1058785 | ||
|
|
87701822ae | ||
|
|
b67f11a3bb | ||
|
|
804e13efff | ||
|
|
504b81b720 | ||
|
|
538e8b588e | ||
|
|
aba50c5c73 | ||
|
|
8ca7719641 | ||
|
|
5dc2eeaf9a | ||
|
|
658e8ac096 |
@@ -137,6 +137,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Rott-Apple",
|
||||
"name": "Rott-Apple",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/67875570?v=4",
|
||||
"profile": "https://github.com/Rott-Apple",
|
||||
"contributions": [
|
||||
"research"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.39.12](https://github.com/RhetTbull/osxphotos/compare/v0.39.11...v0.39.12)
|
||||
|
||||
> 9 January 2021
|
||||
|
||||
- Force cleanup of objects in write_jpeg (fix memory leak) [`#344`](https://github.com/RhetTbull/osxphotos/pull/344)
|
||||
- doc: Recorded screencast and updated of readme [skip ci] [`#328`](https://github.com/RhetTbull/osxphotos/pull/328)
|
||||
- Added PhotoInfo.visible, PhotoInfo.date_trashed, closes #333, #334 [`#333`](https://github.com/RhetTbull/osxphotos/issues/333)
|
||||
- Force cleanup of objects with autorelease pool [`b67f11a`](https://github.com/RhetTbull/osxphotos/commit/b67f11a3bb95c08a39a185b6d884092870e949f2)
|
||||
- Add @Rott-Apple as a contributor [`71cb015`](https://github.com/RhetTbull/osxphotos/commit/71cb01572d2d946df18dd7b36f95b2f2e5b48f86)
|
||||
- Updated README [skip ci] [`804e13e`](https://github.com/RhetTbull/osxphotos/commit/804e13efff921ab51b996493d659b32102807a8a)
|
||||
|
||||
#### [v0.39.11](https://github.com/RhetTbull/osxphotos/compare/v0.39.10...v0.39.11)
|
||||
|
||||
> 8 January 2021
|
||||
|
||||
- All contributors/add kradalby [`#343`](https://github.com/RhetTbull/osxphotos/pull/343)
|
||||
- Ensure keyword list only contains strings, @all-contributors please add @kradalby for code [`#342`](https://github.com/RhetTbull/osxphotos/pull/342)
|
||||
- Added README.rst, closes #331 [`#331`](https://github.com/RhetTbull/osxphotos/issues/331)
|
||||
- Updated tests workflow badge link [`a7678df`](https://github.com/RhetTbull/osxphotos/commit/a7678df3974ff539050f5acb4c94817f525dcd56)
|
||||
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`e6f45f5`](https://github.com/RhetTbull/osxphotos/commit/e6f45f59491d9e805e227af8cbf8ac08ff99fdf0)
|
||||
- Renamed workflow to tests [`f8468c6`](https://github.com/RhetTbull/osxphotos/commit/f8468c63fda930216f73ad5aa8c4aa92edf1adf2)
|
||||
- Merge branch 'master' of github.com:RhetTbull/osxphotos [`5de9d4f`](https://github.com/RhetTbull/osxphotos/commit/5de9d4f90c1102c4fb0099befd6142180f32df3f)
|
||||
- Ensure merge_exif_keywords are str not int [`123ebb2`](https://github.com/RhetTbull/osxphotos/commit/123ebb2cb752bb94291ac2b77e4a327cee996df1)
|
||||
|
||||
#### [v0.39.10](https://github.com/RhetTbull/osxphotos/compare/v0.39.9...v0.39.10)
|
||||
|
||||
> 6 January 2021
|
||||
@@ -17,8 +41,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
> 4 January 2021
|
||||
|
||||
- Added test for Big Sur 16.0.1 database changes [`7deac58`](https://github.com/RhetTbull/osxphotos/commit/7deac581b1f1fb3dc59885b6e1ab9a63b382408d)
|
||||
- Updated all-contributors [`2bf83e4`](https://github.com/RhetTbull/osxphotos/commit/2bf83e4b1fcfadb664ba8988bca4fef7e4c7da12)
|
||||
- Added additional warning to _photoinfo_export [`fb5fb8e`](https://github.com/RhetTbull/osxphotos/commit/fb5fb8ebc73f96548975432333dfdf01c4794d51)
|
||||
- Create terminalizer-demo.yml [`5dc2eea`](https://github.com/RhetTbull/osxphotos/commit/5dc2eeaf9a7265873c81db23bbc86d3023189a26)
|
||||
- doc: Recorded screencast and updated of readme [`658e8ac`](https://github.com/RhetTbull/osxphotos/commit/658e8ac096d141fce48483dbfc1426bea317d806)
|
||||
|
||||
#### [v0.39.8](https://github.com/RhetTbull/osxphotos/compare/v0.39.7...v0.39.8)
|
||||
|
||||
|
||||
80
README.md
80
README.md
@@ -3,45 +3,46 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
- [OSXPhotos](#osxphotos)
|
||||
* [What is osxphotos?](#what-is-osxphotos)
|
||||
* [Supported operating systems](#supported-operating-systems)
|
||||
* [Installation instructions](#installation-instructions)
|
||||
* [Command Line Usage](#command-line-usage)
|
||||
+ [Command line examples](#command-line-examples)
|
||||
+ [Command line reference: export](#command-line-reference-export)
|
||||
* [Package Interface](#package-interface)
|
||||
+ [PhotosDB](#photosdb)
|
||||
+ [PhotoInfo](#photoinfo)
|
||||
+ [ExifInfo](#exifinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [ImportInfo](#importinfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
+ [SearchInfo](#searchinfo)
|
||||
+ [PersonInfo](#personinfo)
|
||||
+ [FaceInfo](#faceinfo)
|
||||
+ [CommentInfo](#commentinfo)
|
||||
+ [LikeInfo](#likeinfo)
|
||||
+ [Raw Photos](#raw-photos)
|
||||
+ [Template Substitutions](#template-substitutions)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
* [Examples](#examples)
|
||||
* [Related Projects](#related-projects)
|
||||
* [Contributing](#contributing)
|
||||
* [Known Bugs](#known-bugs)
|
||||
* [Implementation Notes](#implementation-notes)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
|
||||
<p align="center"><img src="docs/screencast/demo.gif?raw=true" width="713" height="430"/></p>
|
||||
|
||||
# Table of Contents
|
||||
* [Supported operating systems](#supported-operating-systems)
|
||||
* [Installation instructions](#installation-instructions)
|
||||
* [Command Line Usage](#command-line-usage)
|
||||
+ [Command line examples](#command-line-examples)
|
||||
+ [Command line reference: export](#command-line-reference-export)
|
||||
* [Package Interface](#package-interface)
|
||||
+ [PhotosDB](#photosdb)
|
||||
+ [PhotoInfo](#photoinfo)
|
||||
+ [ExifInfo](#exifinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [ImportInfo](#importinfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
+ [SearchInfo](#searchinfo)
|
||||
+ [PersonInfo](#personinfo)
|
||||
+ [FaceInfo](#faceinfo)
|
||||
+ [CommentInfo](#commentinfo)
|
||||
+ [LikeInfo](#likeinfo)
|
||||
+ [Raw Photos](#raw-photos)
|
||||
+ [Template Substitutions](#template-substitutions)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
* [Examples](#examples)
|
||||
* [Related Projects](#related-projects)
|
||||
* [Contributing](#contributing)
|
||||
* [Known Bugs](#known-bugs)
|
||||
* [Implementation Notes](#implementation-notes)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
|
||||
|
||||
|
||||
## What is osxphotos?
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database -- for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
|
||||
## Supported operating systems
|
||||
|
||||
@@ -1577,9 +1578,15 @@ Returns `True` if the picture has been marked as a favorite, otherwise `False`
|
||||
#### `hidden`
|
||||
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
||||
|
||||
#### `visible`
|
||||
Returns `True` if the picture is visible in library, otherwise `False`. e.g. non-selected burst photos are not hidden but also not visible
|
||||
|
||||
#### `intrash`
|
||||
Returns `True` if the picture is in the trash ('Recently Deleted' folder), otherwise `False`
|
||||
|
||||
#### `date_trashed`
|
||||
Returns the date the photo was placed in the trash as a datetime.datetime object or None if photo is not in the trash
|
||||
|
||||
#### `location`
|
||||
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
||||
|
||||
@@ -2584,6 +2591,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://kradalby.no"><img src="https://avatars1.githubusercontent.com/u/98431?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Kristoffer Dalby</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=kradalby" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
||||
BIN
docs/screencast/demo.gif
Normal file
BIN
docs/screencast/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
BIN
docs/screencast/osx-screenshot.png
Normal file
BIN
docs/screencast/osx-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
296
docs/screencast/terminalizer-demo.yml
Normal file
296
docs/screencast/terminalizer-demo.yml
Normal file
@@ -0,0 +1,296 @@
|
||||
# how to use this file? see https://github.com/faressoft/terminalizer
|
||||
|
||||
# running commands:
|
||||
# mkdir trip
|
||||
# osxphotos export --export-by-date --from-date 2021-01-01 trip
|
||||
# du -h trip
|
||||
# find trip | head -20
|
||||
|
||||
|
||||
|
||||
# The configurations that used for the recording, feel free to edit them
|
||||
config:
|
||||
|
||||
# Specify a command to be executed
|
||||
# like `/bin/bash -l`, `ls`, or any other commands
|
||||
# the default is bash for Linux
|
||||
# or powershell.exe for Windows
|
||||
command: zsh
|
||||
|
||||
# Specify the current working directory path
|
||||
# the default is the current working directory path
|
||||
cwd: /Users/aravindo/Downloads
|
||||
|
||||
# Export additional ENV variables
|
||||
env:
|
||||
recording: true
|
||||
|
||||
# Explicitly set the number of columns
|
||||
# or use `auto` to take the current
|
||||
# number of columns of your shell
|
||||
cols: 91
|
||||
|
||||
# Explicitly set the number of rows
|
||||
# or use `auto` to take the current
|
||||
# number of rows of your shell
|
||||
rows: 20
|
||||
|
||||
# Amount of times to repeat GIF
|
||||
# If value is -1, play once
|
||||
# If value is 0, loop indefinitely
|
||||
# If value is a positive number, loop n times
|
||||
repeat: 0
|
||||
|
||||
# Quality
|
||||
# 1 - 100
|
||||
quality: 100
|
||||
|
||||
# Delay between frames in ms
|
||||
# If the value is `auto` use the actual recording delays
|
||||
frameDelay: auto
|
||||
|
||||
# Maximum delay between frames in ms
|
||||
# Ignored if the `frameDelay` isn't set to `auto`
|
||||
# Set to `auto` to prevent limiting the max idle time
|
||||
maxIdleTime: 2000
|
||||
|
||||
# The surrounding frame box
|
||||
# The `type` can be null, window, floating, or solid`
|
||||
# To hide the title use the value null
|
||||
# Don't forget to add a backgroundColor style with a null as type
|
||||
frameBox:
|
||||
type: floating
|
||||
title: ""
|
||||
style:
|
||||
border: 0px black solid
|
||||
# boxShadow: none
|
||||
# margin: 0px
|
||||
|
||||
# Add a watermark image to the rendered gif
|
||||
# You need to specify an absolute path for
|
||||
# the image on your machine or a URL, and you can also
|
||||
# add your own CSS styles
|
||||
watermark:
|
||||
imagePath: null
|
||||
style:
|
||||
position: absolute
|
||||
right: 15px
|
||||
bottom: 15px
|
||||
width: 100px
|
||||
opacity: 0.9
|
||||
|
||||
# Cursor style can be one of
|
||||
# `block`, `underline`, or `bar`
|
||||
cursorStyle: block
|
||||
|
||||
# Font family
|
||||
# You can use any font that is installed on your machine
|
||||
# in CSS-like syntax
|
||||
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"
|
||||
|
||||
# The size of the font
|
||||
fontSize: 12
|
||||
|
||||
# The height of lines
|
||||
lineHeight: 1
|
||||
|
||||
# The spacing between letters
|
||||
letterSpacing: 0
|
||||
|
||||
# Theme
|
||||
theme:
|
||||
background: "transparent"
|
||||
foreground: "#afafaf"
|
||||
cursor: "#c7c7c7"
|
||||
black: "#232628"
|
||||
red: "#fc4384"
|
||||
green: "#b3e33b"
|
||||
yellow: "#ffa727"
|
||||
blue: "#75dff2"
|
||||
magenta: "#ae89fe"
|
||||
cyan: "#708387"
|
||||
white: "#d5d5d0"
|
||||
brightBlack: "#626566"
|
||||
brightRed: "#ff7fac"
|
||||
brightGreen: "#c8ed71"
|
||||
brightYellow: "#ebdf86"
|
||||
brightBlue: "#75dff2"
|
||||
brightMagenta: "#ae89fe"
|
||||
brightCyan: "#b1c6ca"
|
||||
brightWhite: "#f9f9f4"
|
||||
|
||||
# Records, feel free to edit them
|
||||
records:
|
||||
- delay: 100
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 100
|
||||
content: m
|
||||
- delay: 100
|
||||
content: "\bmk"
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: "\e[?2004l\r\r\n"
|
||||
- delay: 9
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 300
|
||||
content: o
|
||||
- delay: 100
|
||||
content: "\bos"
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: h
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: s
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: x
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: b
|
||||
- delay: 100
|
||||
content: 'y'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: a
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: f
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: o
|
||||
- delay: 100
|
||||
content: m
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: d
|
||||
- delay: 100
|
||||
content: a
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: e
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: '2'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '2'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: '-'
|
||||
- delay: 100
|
||||
content: '0'
|
||||
- delay: 100
|
||||
content: '1'
|
||||
- delay: 100
|
||||
content: ' '
|
||||
- delay: 100
|
||||
content: t
|
||||
- delay: 100
|
||||
content: r
|
||||
- delay: 100
|
||||
content: i
|
||||
- delay: 100
|
||||
content: p
|
||||
- delay: 300
|
||||
content: "\e[?2004l\r\r\n"
|
||||
- delay: 500
|
||||
content: "Using last opened Photos library: /Users/user/Pictures/Photos Library.photoslibrary\r\n"
|
||||
- delay: 8204
|
||||
content: "Exporting 79 photos to /Users/user/trip...\r\n"
|
||||
- delay: 321
|
||||
content: "Processed: 79 photos, exported: 80, missing: 0, error: 0\r\nElapsed time: 0.321 seconds\r\n"
|
||||
- delay: 317
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
|
||||
- delay: 4252
|
||||
content: "\e[7mdu -h trip\e[27m"
|
||||
- delay: 487
|
||||
content: "\e[10D\e[27md\e[27mu\e[27m \e[27m-\e[27mh\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[?2004l\r\r\n"
|
||||
- delay: 7
|
||||
content: "229M\ttrip/2021/01/03\r\n712K\ttrip/2021/01/02\r\n7.5M\ttrip/2021/01/01\r\n237M\ttrip/2021/01\r\n237M\ttrip/2021\r\n238M\ttrip\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 4280
|
||||
content: "\e[7mfind trip | head -20\e[27m"
|
||||
- delay: 923
|
||||
content: "\e[20D\e[27mf\e[27mi\e[27mn\e[27md\e[27m \e[27mt\e[27mr\e[27mi\e[27mp\e[27m \e[27m|\e[27m \e[27mh\e[27me\e[27ma\e[27md\e[27m \e[27m-\e[27m2\e[27m0\e[?2004l\r\r\n"
|
||||
- delay: 5
|
||||
content: "trip\r\ntrip/2021\r\ntrip/2021/01\r\ntrip/2021/01/03\r\ntrip/2021/01/03/IMG_1234 (1).HEIC\r\ntrip/2021/01/03/IMG_1267.HEIC\r\ntrip/2021/01/03/IMG_1226.HEIC\r\ntrip/2021/01/03/IMG_1271.HEIC\r\ntrip/2021/01/03/IMG_1232 (1).JPG\r\ntrip/2021/01/03/IMG_1270.HEIC\r\ntrip/2021/01/03/IMG_1231.HEIC\r\ntrip/2021/01/03/IMG_6926.JPG\r\ntrip/2021/01/03/IMG_6932.JPG\r\ntrip/2021/01/03/IMG_1266.HEIC\r\ntrip/2021/01/03/IMG_6933.JPG\r\ntrip/2021/01/03/IMG_6927.JPG\r\ntrip/2021/01/03/IMG_1233 (1).JPG\r\ntrip/2021/01/03/IMG_1228 (1).HEIC\r\ntrip/2021/01/03/IMG_6931.JPG\r\ntrip/2021/01/03/IMG_6930.JPG\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]7;file://wingeier-macOS/Users/aravindo/Downloads\a\r\e[0m\e[27m\e[24m\e[J❯ \e[K\e[?2004h"
|
||||
- delay: 3615
|
||||
content: "\e[?2004l\r\r\n"
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.11"
|
||||
__version__ = "0.39.13"
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
""" ImageConverter class
|
||||
Convert an image to JPEG using CoreImage --
|
||||
Convert an image to JPEG using CoreImage --
|
||||
for example, RAW to JPEG. Only works if Mac equipped with GPU. """
|
||||
|
||||
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
||||
|
||||
import pathlib
|
||||
|
||||
import objc
|
||||
import Metal
|
||||
import Quartz
|
||||
from Cocoa import NSURL
|
||||
@@ -20,6 +21,7 @@ class ImageConversionError(Exception):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
""" Convert images to jpeg. This class is a singleton
|
||||
which will re-use the Core Image CIContext to avoid
|
||||
@@ -67,49 +69,58 @@ class ImageConverter:
|
||||
ImageConversionError if error during conversion
|
||||
"""
|
||||
|
||||
# accept input_path or output_path as pathlib.Path
|
||||
if not isinstance(input_path, str):
|
||||
input_path = str(input_path)
|
||||
# Set up a dedicated objc autorelease pool for this function call.
|
||||
# This is to ensure that all the NSObjects are cleaned up after each
|
||||
# call to prevent memory leaks.
|
||||
# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
|
||||
# https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management
|
||||
with objc.autorelease_pool():
|
||||
# accept input_path or output_path as pathlib.Path
|
||||
if not isinstance(input_path, str):
|
||||
input_path = str(input_path)
|
||||
|
||||
if not isinstance(output_path, str):
|
||||
output_path = str(output_path)
|
||||
if not isinstance(output_path, str):
|
||||
output_path = str(output_path)
|
||||
|
||||
if not pathlib.Path(input_path).is_file():
|
||||
raise FileNotFoundError(f"could not find {input_path}")
|
||||
if not pathlib.Path(input_path).is_file():
|
||||
raise FileNotFoundError(f"could not find {input_path}")
|
||||
|
||||
if not (0.0 <= compression_quality <= 1.0):
|
||||
raise ValueError(
|
||||
"illegal value for compression_quality: {compression_quality}"
|
||||
if not (0.0 <= compression_quality <= 1.0):
|
||||
raise ValueError(
|
||||
"illegal value for compression_quality: {compression_quality}"
|
||||
)
|
||||
|
||||
input_url = NSURL.fileURLWithPath_(input_path)
|
||||
output_url = NSURL.fileURLWithPath_(output_path)
|
||||
|
||||
with pipes() as (out, err):
|
||||
# capture stdout and stderr from system calls
|
||||
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
|
||||
# prints to stderr something like:
|
||||
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
|
||||
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
if input_image is None:
|
||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
)
|
||||
|
||||
input_url = NSURL.fileURLWithPath_(input_path)
|
||||
output_url = NSURL.fileURLWithPath_(output_path)
|
||||
|
||||
with pipes() as (out, err):
|
||||
# capture stdout and stderr from system calls
|
||||
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
|
||||
# prints to stderr something like:
|
||||
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
|
||||
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
if input_image is None:
|
||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
)
|
||||
|
||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
|
||||
)
|
||||
_, error = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
|
||||
input_image, output_url, output_colorspace, output_options, None
|
||||
)
|
||||
if not error:
|
||||
return True
|
||||
else:
|
||||
raise ImageConversionError(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
|
||||
)
|
||||
(
|
||||
_,
|
||||
error,
|
||||
) = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
|
||||
input_image, output_url, output_colorspace, output_options, None
|
||||
)
|
||||
if not error:
|
||||
return True
|
||||
else:
|
||||
raise ImageConversionError(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
)
|
||||
|
||||
|
||||
@@ -113,15 +113,15 @@ class PhotoInfo:
|
||||
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
|
||||
# only report lastmodified date for Photos <=4 if photo is edited;
|
||||
# even in this case, the date could be incorrect
|
||||
if self.hasadjustments or self._db._db_version > _PHOTOS_4_VERSION:
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
if not self.hasadjustments and self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -501,37 +501,52 @@ class PhotoInfo:
|
||||
downloaded from cloud to local storate their status in the database might still show
|
||||
isMissing = 1
|
||||
"""
|
||||
return True if self._info["isMissing"] == 1 else False
|
||||
return self._info["isMissing"] == 1
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
""" True if picture has adjustments / edits """
|
||||
return True if self._info["hasAdjustments"] == 1 else False
|
||||
return self._info["hasAdjustments"] == 1
|
||||
|
||||
@property
|
||||
def external_edit(self):
|
||||
""" Returns True if picture was edited outside of Photos using external editor """
|
||||
return (
|
||||
True
|
||||
if self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
else False
|
||||
)
|
||||
return self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
""" True if picture is marked as favorite """
|
||||
return True if self._info["favorite"] == 1 else False
|
||||
return self._info["favorite"] == 1
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" True if picture is hidden """
|
||||
return True if self._info["hidden"] == 1 else False
|
||||
return self._info["hidden"] == 1
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
""" True if picture is visble """
|
||||
return self._info["visible"]
|
||||
|
||||
@property
|
||||
def intrash(self):
|
||||
""" True if picture is in trash ('Recently Deleted' folder)"""
|
||||
return self._info["intrash"]
|
||||
|
||||
@property
|
||||
def date_trashed(self):
|
||||
""" Date asset was placed in the trash or None """
|
||||
# TODO: add add_timezone(dt, offset_seconds) to datetime_utils
|
||||
# also update date_modified
|
||||
trasheddate = self._info["trasheddate"]
|
||||
if trasheddate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return trasheddate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
@@ -551,14 +566,15 @@ class PhotoInfo:
|
||||
"""Returns Uniform Type Identifier (UTI) for the image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif self.has_raw and self.raw_original:
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
and self.raw_original
|
||||
):
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
|
||||
@@ -597,12 +613,12 @@ class PhotoInfo:
|
||||
@property
|
||||
def ismovie(self):
|
||||
"""Returns True if file is a movie, otherwise False"""
|
||||
return True if self._info["type"] == _MOVIE_TYPE else False
|
||||
return self._info["type"] == _MOVIE_TYPE
|
||||
|
||||
@property
|
||||
def isphoto(self):
|
||||
"""Returns True if file is an image, otherwise False"""
|
||||
return True if self._info["type"] == _PHOTO_TYPE else False
|
||||
return self._info["type"] == _PHOTO_TYPE
|
||||
|
||||
@property
|
||||
def incloud(self):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# add original=False to export instead of version= (and maybe others like path())
|
||||
# make burst/live methods get uuid from self instead of passing as arg
|
||||
|
||||
import copy
|
||||
import pathlib
|
||||
import threading
|
||||
import time
|
||||
@@ -169,12 +170,14 @@ class ImageData:
|
||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.metadata = None
|
||||
self.uti = None
|
||||
self.image_data = None
|
||||
self.info = None
|
||||
self.orientation = None
|
||||
def __init__(
|
||||
self, metadata=None, uti=None, image_data=None, info=None, orientation=None
|
||||
):
|
||||
self.metadata = metadata
|
||||
self.uti = uti
|
||||
self.image_data = image_data
|
||||
self.info = info
|
||||
self.orientation = orientation
|
||||
|
||||
|
||||
class AVAssetData:
|
||||
@@ -475,44 +478,48 @@ class PhotoAsset:
|
||||
# if self.live:
|
||||
# raise NotImplementedError("Live photos not implemented yet")
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
imagedata = self._request_image_data(version=version)
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
output_file = None
|
||||
if self.isphoto:
|
||||
imagedata = self._request_image_data(version=version)
|
||||
ext = get_preferred_uti_extension(imagedata.uti)
|
||||
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
output_file = dest / f"{filename.stem}.{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(imagedata.image_data)
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
with open(output_file, "wb") as fd:
|
||||
fd.write(imagedata.image_data)
|
||||
del imagedata
|
||||
elif self.ismovie:
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
return [str(output_file)]
|
||||
|
||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
""" Request image data and metadata for self._phasset
|
||||
@@ -529,50 +536,56 @@ class PhotoAsset:
|
||||
|
||||
# reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc
|
||||
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
with objc.autorelease_pool():
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
|
||||
# pylint: disable=no-member
|
||||
options_request = Photos.PHImageRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setSynchronous_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = ImageData()
|
||||
event = threading.Event()
|
||||
|
||||
def handler(imageData, dataUTI, orientation, info):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
nonlocal requestdata
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
options_request = Photos.PHImageRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setSynchronous_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata.uti = dataUTI
|
||||
requestdata.orientation = orientation
|
||||
requestdata.info = info
|
||||
requestdata.image_data = imageData
|
||||
requestdata = ImageData()
|
||||
event = threading.Event()
|
||||
|
||||
event.set()
|
||||
def handler(imageData, dataUTI, orientation, info):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
self._imagedata = requestdata
|
||||
return requestdata
|
||||
nonlocal requestdata
|
||||
|
||||
options = {}
|
||||
# pylint: disable=no-member
|
||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||
imgSrc, 0, options
|
||||
)
|
||||
requestdata.uti = dataUTI
|
||||
requestdata.orientation = orientation
|
||||
requestdata.info = info
|
||||
requestdata.image_data = imageData
|
||||
|
||||
event.set()
|
||||
|
||||
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
# options_request.dealloc()
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
def _make_result_handle_(self, data):
|
||||
""" Make handler function and threading event to use with
|
||||
@@ -634,37 +647,41 @@ class SlowMoVideoExporter(NSObject):
|
||||
Returns:
|
||||
path to exported file
|
||||
"""
|
||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
)
|
||||
exporter.setOutputURL_(self.url)
|
||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||
exporter.setShouldOptimizeForNetworkUse_(True)
|
||||
|
||||
self.done = False
|
||||
with objc.autorelease_pool():
|
||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||
)
|
||||
exporter.setOutputURL_(self.url)
|
||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||
exporter.setShouldOptimizeForNetworkUse_(True)
|
||||
|
||||
def handler():
|
||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||
self.done = True
|
||||
self.done = False
|
||||
|
||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||
# wait for export to complete
|
||||
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
||||
# but I can't figure out how to make that work and this does work
|
||||
while True:
|
||||
status = exporter.status()
|
||||
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
||||
break
|
||||
elif status not in (
|
||||
AVFoundation.AVAssetExportSessionStatusWaiting,
|
||||
AVFoundation.AVAssetExportSessionStatusExporting,
|
||||
):
|
||||
raise PhotoKitExportError(
|
||||
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
||||
)
|
||||
time.sleep(MIN_SLEEP)
|
||||
def handler():
|
||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||
self.done = True
|
||||
|
||||
return NSURL_to_path(exporter.outputURL())
|
||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||
# wait for export to complete
|
||||
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
||||
# but I can't figure out how to make that work and this does work
|
||||
while True:
|
||||
status = exporter.status()
|
||||
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
||||
break
|
||||
elif status not in (
|
||||
AVFoundation.AVAssetExportSessionStatusWaiting,
|
||||
AVFoundation.AVAssetExportSessionStatusExporting,
|
||||
):
|
||||
raise PhotoKitExportError(
|
||||
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
||||
)
|
||||
time.sleep(MIN_SLEEP)
|
||||
|
||||
exported_path = NSURL_to_path(exporter.outputURL())
|
||||
# exporter.dealloc()
|
||||
return exported_path
|
||||
|
||||
def __del__(self):
|
||||
self.avasset = None
|
||||
@@ -701,39 +718,43 @@ class VideoAsset(PhotoAsset):
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest, filename=filename, version=version, overwrite=overwrite
|
||||
)
|
||||
]
|
||||
with objc.autorelease_pool():
|
||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||
return [
|
||||
self._export_slow_mo(
|
||||
dest, filename=filename, version=version, overwrite=overwrite
|
||||
)
|
||||
]
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
output_file = None
|
||||
videodata = self._request_video_data(version=version)
|
||||
if videodata.asset is None:
|
||||
raise PhotoKitExportError("Could not get video for asset")
|
||||
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
url = videodata.asset.URL()
|
||||
path = pathlib.Path(NSURL_to_path(url))
|
||||
del videodata
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("Could not get path to video file")
|
||||
ext = path.suffix
|
||||
output_file = dest / f"{filename.stem}{ext}"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
FileUtil.copy(path, output_file)
|
||||
FileUtil.copy(path, output_file)
|
||||
|
||||
return [str(output_file)]
|
||||
return [str(output_file)]
|
||||
|
||||
def _export_slow_mo(
|
||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||
@@ -752,33 +773,38 @@ class VideoAsset(PhotoAsset):
|
||||
Raises:
|
||||
ValueError if dest is not a valid directory
|
||||
"""
|
||||
if not self.slow_mo:
|
||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||
with objc.autorelease_pool():
|
||||
if not self.slow_mo:
|
||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||
|
||||
videodata = self._request_video_data(version=version)
|
||||
if (
|
||||
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
||||
or len(videodata.asset.tracks()) != 2
|
||||
):
|
||||
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
||||
videodata = self._request_video_data(version=version)
|
||||
if (
|
||||
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
||||
or len(videodata.asset.tracks()) != 2
|
||||
):
|
||||
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
||||
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
output_file = dest / f"{filename.stem}.mov"
|
||||
output_file = dest / f"{filename.stem}.mov"
|
||||
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
if not overwrite:
|
||||
output_file = pathlib.Path(increment_filename(output_file))
|
||||
|
||||
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
||||
videodata.asset, output_file
|
||||
)
|
||||
return exporter.exportSlowMoVideo()
|
||||
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
||||
videodata.asset, output_file
|
||||
)
|
||||
video = exporter.exportSlowMoVideo()
|
||||
# exporter.dealloc()
|
||||
return video
|
||||
|
||||
# todo: rewrite this with NotificationCenter and App event loop?
|
||||
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||
@@ -793,38 +819,43 @@ class VideoAsset(PhotoAsset):
|
||||
Raises:
|
||||
ValueError if passed invalid value for version
|
||||
"""
|
||||
with objc.autorelease_pool():
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
|
||||
if version not in [
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
]:
|
||||
raise ValueError("Invalid value for version")
|
||||
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = AVAssetData()
|
||||
event = threading.Event()
|
||||
|
||||
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
||||
options_request.setNetworkAccessAllowed_(True)
|
||||
options_request.setVersion_(version)
|
||||
options_request.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
requestdata = AVAssetData()
|
||||
event = threading.Event()
|
||||
def handler(asset, audiomix, info):
|
||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||
nonlocal requestdata
|
||||
|
||||
def handler(asset, audiomix, info):
|
||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||
nonlocal requestdata
|
||||
requestdata.asset = asset
|
||||
requestdata.audiomix = audiomix
|
||||
requestdata.info = info
|
||||
|
||||
requestdata.asset = asset
|
||||
requestdata.audiomix = audiomix
|
||||
requestdata.info = info
|
||||
event.set()
|
||||
|
||||
event.set()
|
||||
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
|
||||
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
||||
self.phasset, options_request, handler
|
||||
)
|
||||
event.wait()
|
||||
return requestdata
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
|
||||
class LivePhotoRequest(NSObject):
|
||||
@@ -843,47 +874,54 @@ class LivePhotoRequest(NSObject):
|
||||
|
||||
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
|
||||
""" return the photos and video components of a live video as [PHAssetResource] """
|
||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
options.setVersion_(version)
|
||||
options.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
delegate = PhotoKitNotificationDelegate.alloc().init()
|
||||
|
||||
self.nc.addObserver_selector_name_object_(
|
||||
delegate, "liveNotification:", None, None
|
||||
)
|
||||
|
||||
self.live_photo = None
|
||||
|
||||
def handler(result, info):
|
||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||
if not info["PHImageResultIsDegradedKey"]:
|
||||
self.live_photo = result
|
||||
self.info = info
|
||||
self.nc.postNotificationName_object_(
|
||||
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
||||
)
|
||||
|
||||
try:
|
||||
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
self.asset,
|
||||
Photos.PHImageManagerMaximumSize,
|
||||
Photos.PHImageContentModeDefault,
|
||||
options,
|
||||
handler,
|
||||
with objc.autorelease_pool():
|
||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
options.setVersion_(version)
|
||||
options.setDeliveryMode_(
|
||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||
)
|
||||
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
||||
except KeyboardInterrupt:
|
||||
AppHelper.stopEventLoop()
|
||||
finally:
|
||||
pass
|
||||
delegate = PhotoKitNotificationDelegate.alloc().init()
|
||||
|
||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||
self.live_photo
|
||||
)
|
||||
return asset_resources
|
||||
self.nc.addObserver_selector_name_object_(
|
||||
delegate, "liveNotification:", None, None
|
||||
)
|
||||
|
||||
self.live_photo = None
|
||||
|
||||
def handler(result, info):
|
||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||
if not info["PHImageResultIsDegradedKey"]:
|
||||
self.live_photo = result
|
||||
self.info = info
|
||||
self.nc.postNotificationName_object_(
|
||||
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
||||
)
|
||||
|
||||
try:
|
||||
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
||||
self.asset,
|
||||
Photos.PHImageManagerMaximumSize,
|
||||
Photos.PHImageContentModeDefault,
|
||||
options,
|
||||
handler,
|
||||
)
|
||||
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
||||
except KeyboardInterrupt:
|
||||
AppHelper.stopEventLoop()
|
||||
finally:
|
||||
pass
|
||||
|
||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||
self.live_photo
|
||||
)
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(asset_resources)
|
||||
del asset_resources
|
||||
return data
|
||||
|
||||
def __del__(self):
|
||||
self.manager = None
|
||||
@@ -923,88 +961,99 @@ class LivePhotoAsset(PhotoAsset):
|
||||
ValueError if dest is not a valid directory
|
||||
PhotoKitExportError if error during export
|
||||
"""
|
||||
filename = (
|
||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
with objc.autorelease_pool():
|
||||
filename = (
|
||||
pathlib.Path(filename)
|
||||
if filename
|
||||
else pathlib.Path(self.original_filename)
|
||||
)
|
||||
|
||||
photo_ext = get_preferred_uti_extension(photo_resource.uniformTypeIdentifier())
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(video_resource.uniformTypeIdentifier())
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
dest = pathlib.Path(dest)
|
||||
if not dest.is_dir():
|
||||
raise ValueError("dest must be a valid directory: {dest}")
|
||||
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||
self._manager, self.phasset
|
||||
)
|
||||
resources = request.requestLivePhotoResources(version=version)
|
||||
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
video_resource = None
|
||||
photo_resource = None
|
||||
for resource in resources:
|
||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||
video_resource = resource
|
||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||
photo_resource = resource
|
||||
|
||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# exported = []
|
||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||
# causes pathlib.Path().is_file() to fail in tests
|
||||
if not video_resource or not photo_resource:
|
||||
raise PhotoKitExportError(
|
||||
"Did not find photo/video resources for live photo"
|
||||
)
|
||||
|
||||
# if photo:
|
||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# photo_resource, photo_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(photo_output_file))
|
||||
photo_ext = get_preferred_uti_extension(
|
||||
photo_resource.uniformTypeIdentifier()
|
||||
)
|
||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||
video_ext = get_preferred_uti_extension(
|
||||
video_resource.uniformTypeIdentifier()
|
||||
)
|
||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||
|
||||
# if video:
|
||||
# video_output_url = path_to_NSURL(video_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# video_resource, video_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(video_output_file))
|
||||
if not overwrite:
|
||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||
|
||||
# def completion_handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
# def handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
# options.setNetworkAccessAllowed_(True)
|
||||
# exported = []
|
||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||
# causes pathlib.Path().is_file() to fail in tests
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
# if photo:
|
||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# photo_resource, photo_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(photo_output_file))
|
||||
|
||||
return exported
|
||||
# if video:
|
||||
# video_output_url = path_to_NSURL(video_output_file)
|
||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||
# video_resource, video_output_url, options, handler
|
||||
# )
|
||||
# exported.append(str(video_output_file))
|
||||
|
||||
# def completion_handler(error):
|
||||
# if error:
|
||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||
|
||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||
|
||||
exported = []
|
||||
if photo:
|
||||
data = self._request_resource_data(photo_resource)
|
||||
# image_data = self.request_image_data(version=version)
|
||||
with open(photo_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(photo_output_file))
|
||||
del data
|
||||
if video:
|
||||
data = self._request_resource_data(video_resource)
|
||||
with open(video_output_file, "wb") as fd:
|
||||
fd.write(data)
|
||||
exported.append(str(video_output_file))
|
||||
del data
|
||||
|
||||
request.dealloc()
|
||||
return exported
|
||||
|
||||
def _request_resource_data(self, resource):
|
||||
""" Request asset resource data (either photo or video component)
|
||||
@@ -1015,33 +1064,40 @@ class LivePhotoAsset(PhotoAsset):
|
||||
Raises:
|
||||
"""
|
||||
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
with objc.autorelease_pool():
|
||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||
options.setNetworkAccessAllowed_(True)
|
||||
|
||||
requestdata = PHAssetResourceData()
|
||||
event = threading.Event()
|
||||
requestdata = PHAssetResourceData()
|
||||
event = threading.Event()
|
||||
|
||||
def handler(data):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
def handler(data):
|
||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||
|
||||
nonlocal requestdata
|
||||
nonlocal requestdata
|
||||
|
||||
requestdata.data += data
|
||||
requestdata.data += data
|
||||
|
||||
def completion_handler(error):
|
||||
if error:
|
||||
raise PhotoKitExportError("Error requesting data for asset resource")
|
||||
event.set()
|
||||
def completion_handler(error):
|
||||
if error:
|
||||
raise PhotoKitExportError(
|
||||
"Error requesting data for asset resource"
|
||||
)
|
||||
event.set()
|
||||
|
||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||
resource, options, handler, completion_handler
|
||||
)
|
||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||
resource, options, handler, completion_handler
|
||||
)
|
||||
|
||||
event.wait()
|
||||
options.dealloc()
|
||||
return requestdata.data
|
||||
event.wait()
|
||||
|
||||
# not sure why this is needed -- some weird ref count thing maybe
|
||||
# if I don't do this, memory leaks
|
||||
data = copy.copy(requestdata.data)
|
||||
del requestdata
|
||||
return data
|
||||
|
||||
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
|
||||
# # Returns an NSImage which isn't overly useful
|
||||
@@ -1127,19 +1183,20 @@ class PhotoLibrary:
|
||||
"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||
uuid_list, fetch_options
|
||||
)
|
||||
if fetch_result and fetch_result.count() >= 1:
|
||||
return [
|
||||
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
||||
for idx in range(fetch_result.count())
|
||||
]
|
||||
else:
|
||||
raise PhotoKitFetchFailed(
|
||||
f"Fetch did not return result for uuid_list {uuid_list}"
|
||||
with objc.autorelease_pool():
|
||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||
uuid_list, fetch_options
|
||||
)
|
||||
if fetch_result and fetch_result.count() >= 1:
|
||||
return [
|
||||
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
||||
for idx in range(fetch_result.count())
|
||||
]
|
||||
else:
|
||||
raise PhotoKitFetchFailed(
|
||||
f"Fetch did not return result for uuid_list {uuid_list}"
|
||||
)
|
||||
|
||||
def fetch_uuid(self, uuid):
|
||||
""" fetch PHAsset with uuid = uuid
|
||||
|
||||
@@ -887,7 +887,9 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.fileSize,
|
||||
RKVersion.subType
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -915,7 +917,9 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.originalFileSize,
|
||||
RKVersion.subType
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -962,6 +966,8 @@ class PhotosDB:
|
||||
# 38 RKMaster.orientation,
|
||||
# 39 RKMaster.originalFileSize
|
||||
# 40 RKVersion.subType
|
||||
# 41 RKVersion.inTrashDate
|
||||
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1136,7 +1142,14 @@ class PhotosDB:
|
||||
)
|
||||
|
||||
# recently deleted items
|
||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||
self._dbphotos[uuid]["intrash"] = row[32] == 1
|
||||
self._dbphotos[uuid]["trasheddate_timestamp"] = row[41]
|
||||
try:
|
||||
self._dbphotos[uuid]["trasheddate"] = datetime.fromtimestamp(
|
||||
row[41] + TIME_DELTA
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
self._dbphotos[uuid]["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
self._dbphotos[uuid]["height"] = row[33]
|
||||
@@ -1147,6 +1160,10 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||
|
||||
# visibility state
|
||||
self._dbphotos[uuid]["visibility_state"] = row[42]
|
||||
self._dbphotos[uuid]["visible"] = row[42] == 1
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
@@ -1840,7 +1857,9 @@ class PhotosDB:
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
|
||||
{depth_state},
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP,
|
||||
{asset_table}.ZVISIBILITYSTATE,
|
||||
{asset_table}.ZTRASHEDDATE
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1885,6 +1904,8 @@ class PhotosDB:
|
||||
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
|
||||
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
|
||||
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
|
||||
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1901,9 +1922,7 @@ class PhotosDB:
|
||||
info["lastmodifieddate_timestamp"] = row[37]
|
||||
try:
|
||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
|
||||
except ValueError:
|
||||
info["lastmodifieddate"] = None
|
||||
except TypeError:
|
||||
except (ValueError, TypeError):
|
||||
info["lastmodifieddate"] = None
|
||||
|
||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||
@@ -2046,6 +2065,11 @@ class PhotosDB:
|
||||
|
||||
# recently deleted items
|
||||
info["intrash"] = True if row[28] == 1 else False
|
||||
info["trasheddate_timestamp"] = row[39]
|
||||
try:
|
||||
info["trasheddate"] = datetime.fromtimestamp(row[39] + TIME_DELTA)
|
||||
except (ValueError, TypeError):
|
||||
info["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
info["height"] = row[29]
|
||||
@@ -2056,6 +2080,11 @@ class PhotosDB:
|
||||
info["original_orientation"] = row[34]
|
||||
info["original_filesize"] = row[35]
|
||||
|
||||
# visibility state, visible (True) if 0, otherwise not visible (False)
|
||||
# only values I've seen are 0 for visible, 2 for not-visible
|
||||
info["visibility_state"] = row[38]
|
||||
info["visible"] = row[38] == 0
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
|
||||
@@ -63,8 +63,8 @@ def noop(*args, **kwargs):
|
||||
|
||||
|
||||
def _get_os_version():
|
||||
# returns tuple containing OS version
|
||||
# e.g. 10.13.6 = (10, 13, 6)
|
||||
# returns tuple of str containing OS version
|
||||
# e.g. 10.13.6 = ("10", "13", "6")
|
||||
version = platform.mac_ver()[0].split(".")
|
||||
if len(version) == 2:
|
||||
(ver, major) = version
|
||||
@@ -260,10 +260,10 @@ def get_preferred_uti_extension(uti):
|
||||
returns: preferred extension as str """
|
||||
|
||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||
|
||||
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||
uti, CoreServices.kUTTagClassFilenameExtension
|
||||
)
|
||||
with objc.autorelease_pool():
|
||||
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||
uti, CoreServices.kUTTagClassFilenameExtension
|
||||
)
|
||||
|
||||
|
||||
def findfiles(pattern, path_):
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,6 +13,11 @@ import pytest
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.utils import _get_os_version
|
||||
|
||||
OS_VERSION = _get_os_version()
|
||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
|
||||
PHOTOS_DB = "tests/Test-10.15.7.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.15.7.photoslibrary/database/photos.db"
|
||||
@@ -98,6 +103,11 @@ UUID_DICT = {
|
||||
"movie": "D1359D09-1373-4F3B-B0E3-1A4DE573E4A3",
|
||||
}
|
||||
|
||||
UUID_DICT_LOCAL = {
|
||||
"not_visible": "ABF00253-78E7-4FD6-953B-709307CD489D",
|
||||
"burst": "44AF1FCA-AC2D-4FA5-B288-E67DC18F9CA8",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
@@ -194,6 +204,11 @@ def photosdb():
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb_local():
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_LOCAL)
|
||||
|
||||
|
||||
def test_init1():
|
||||
# test named argument
|
||||
|
||||
@@ -413,6 +428,22 @@ def test_not_hidden(photosdb):
|
||||
assert p.hidden == False
|
||||
|
||||
|
||||
def test_visible(photosdb):
|
||||
""" test visible """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.visible
|
||||
|
||||
|
||||
def test_not_burst(photosdb):
|
||||
""" test not burst """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert not p.burst
|
||||
|
||||
|
||||
def test_location_1(photosdb):
|
||||
# test photo with lat/lon info
|
||||
|
||||
@@ -546,6 +577,7 @@ def test_photoinfo_intrash_1(photosdb):
|
||||
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert p.date_trashed.isoformat() == "2120-06-10T11:24:47.685857-05:00"
|
||||
|
||||
|
||||
def test_photoinfo_intrash_2(photosdb):
|
||||
@@ -587,6 +619,7 @@ def test_photoinfo_not_intrash(photosdb):
|
||||
|
||||
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||
assert not p.intrash
|
||||
assert p.date_trashed is None
|
||||
|
||||
|
||||
def test_keyword_2(photosdb):
|
||||
@@ -973,7 +1006,7 @@ def test_from_to_date(photosdb):
|
||||
time.tzset()
|
||||
|
||||
photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28))
|
||||
assert len(photos) == 10
|
||||
assert len(photos) == 10
|
||||
|
||||
photos = photosdb.photos(to_date=datetime.datetime(2018, 10, 28))
|
||||
assert len(photos) == 7
|
||||
@@ -1133,3 +1166,22 @@ def test_original_filename(photosdb):
|
||||
assert photo.original_filename == ORIGINAL_FILENAME_DICT["filename"]
|
||||
photo._info["originalFilename"] = original_filename
|
||||
|
||||
|
||||
# The following tests only run on the author's personal library
|
||||
# They test things difficult to test in the test libraries
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_not_visible_burst(photosdb_local):
|
||||
""" test not visible and burst (needs image from local library) """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["not_visible"])
|
||||
assert not photo.visible
|
||||
assert photo.burst
|
||||
|
||||
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_visible_burst(photosdb_local):
|
||||
""" test not visible and burst (needs image from local library) """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst"])
|
||||
assert photo.visible
|
||||
assert photo.burst
|
||||
assert len(photo.burst_photos) == 4
|
||||
|
||||
|
||||
@@ -2,14 +2,15 @@ import os
|
||||
import pytest
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.utils import _get_os_version
|
||||
|
||||
skip_test = False if "OSXPHOTOS_TEST_EXPORT" in os.environ else True
|
||||
OS_VERSION = _get_os_version()
|
||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
skip_test, reason="These tests only run against system photos library"
|
||||
SKIP_TEST, reason="These tests only run against system photos library"
|
||||
)
|
||||
|
||||
PHOTOS_DB = "/Users/rhet/Pictures/Photos Library.photoslibrary"
|
||||
|
||||
UUID_DICT = {
|
||||
"has_adjustments": "2B2D5434-6D31-49E2-BF47-B973D34A317B",
|
||||
"no_adjustments": "A8D646C3-89A9-4D74-8001-4EB46BA55B94",
|
||||
@@ -21,8 +22,7 @@ UUID_DICT = {
|
||||
def photosdb():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
return photosdb
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
|
||||
def test_export_default_name(photosdb):
|
||||
@@ -326,6 +326,22 @@ def test_not_hidden(photosdb):
|
||||
assert p.hidden == False
|
||||
|
||||
|
||||
def test_visible(photosdb):
|
||||
""" test visible """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.visible
|
||||
|
||||
|
||||
def test_not_burst(photosdb):
|
||||
""" test not burst """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert not p.burst
|
||||
|
||||
|
||||
def test_location_1(photosdb):
|
||||
# test photo with lat/lon info
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
|
||||
@@ -417,6 +433,7 @@ def test_photos_intrash_2(photosdb):
|
||||
photos = photosdb.photos(intrash=True)
|
||||
for p in photos:
|
||||
assert p.intrash
|
||||
assert p.date_trashed.isoformat() == "2305-12-17T13:19:08.978144-07:00"
|
||||
|
||||
|
||||
def test_photos_not_intrash(photosdb):
|
||||
@@ -424,6 +441,7 @@ def test_photos_not_intrash(photosdb):
|
||||
photos = photosdb.photos(intrash=False)
|
||||
for p in photos:
|
||||
assert not p.intrash
|
||||
assert p.date_trashed is None
|
||||
|
||||
|
||||
def test_photoinfo_intrash_1(photosdb):
|
||||
|
||||
Reference in New Issue
Block a user