Jak usunąć dużą liczbę nieużywanych słów kluczowych z aplikacji Zdjęcia Apple?

1

Moja biblioteka zdjęć ewoluowała przez wiele lat, początkowo jako biblioteka iPhoto, następnie połączona z biblioteką Aperture, a następnie stała się biblioteką zdjęć. W ciągu ostatnich 20 lat zgromadziłem tysiące słów kluczowych, które nie zawierają żadnych zdjęć.

Sama aplikacja Zdjęcia nie obsługuje funkcji „Usuń nieużywane słowa kluczowe”. Spojrzałem na to, aby to zrobić za pomocą AppleScript, ale pomimo tego, że jestem dość doświadczonym programistą, nie potrafiłem tego zrobić (AppleScript doprowadza mnie do szału. szczerze).

Mam nadzieję, że ktoś już napisał taki skrypt, a jeśli nie, to jakieś narzędzie, które zrobi to za mnie.

Dave Sag
źródło
Tak będzie dobrze. Każda pomoc doceniona.
Dave Sag

Odpowiedzi:

1

Poniżej znajduje się skrypt, który napisałem i przetestowałem przez kilka dni. Chociaż Photos.app firmy Apple jest skryptowalny, już zauważyłeś, że brakuje w nim niezbędnych metod usuwania nieużywanych słów kluczowych. Jeśli znasz AppleScript i pojęcie skryptów interfejsu użytkownika, wydaje się, że jest to jedyna dostępna opcja.

Uwaga: Aby skrypty interfejsu użytkownika działały, musisz zapewnić niezbędne uprawnienia dostępu do edytora skryptów .

Mój osobisty pogląd na skrypty interfejsu użytkownika jest ogólnie negatywny, ale dołożyłem wszelkich starań, aby złagodzić typową temperamentalną naturę i kruchość skryptów interfejsu użytkownika, i wykonałem kilka testów w moim systemie, aby zaobserwować dość płynne działanie.

Jednak jedną szczególną funkcją, której nie mogłem (nie chciałem) sprawdzić podczas testowania, jest to, jak działa skrypt, gdy istnieją tysiące słów kluczowych i / lub bardzo duża biblioteka Zdjęć . Sam mam bibliotekę zdjęć składającą się z mniej niż 100 zdjęć i żadne z nich nie było oznaczone słowami kluczowymi, dlatego stworzyłem próbkę 20, z których losowo przypisałem około połowy z nich.

Teoretycznie jedynym czynnikiem wpływającym na wielkość biblioteki lub liczbę słów kluczowych na działanie skryptu jest czas wykonywania. Jednak w AppleScript mogą wystąpić problemy z przekroczeniem limitu czasu, które przedwcześnie przerywają skrypt; a w przypadku skryptów interfejsu użytkownika prawdopodobieństwo zgłoszenia błędu zwykle wzrasta wraz z czasem działania.

Szczegółowe uwagi dotyczące tych niepewności dotyczących wydajności znajdują się poniżej skryptu. Jeśli napotkasz jakiekolwiek problemy, zgłoś to, a ja rozważę, jak wdrożyć poprawkę. Nie powinno być żadnych negatywnych skutków, gdyby skrypt nie działał idealnie (tzn. Nie stracisz żadnych zdjęć). Nieoptymalna wydajność powinna jedynie doprowadzić do niepełnego oczyszczenia słów kluczowych.

#!/usr/bin/osascript
--------------------------------------------------------------------------------
# pnam: PHOTOS#DELETE UNUSED KEYWORDS
# nmxt: .applescript
# pDSC: A UI scripting-dependent script to remove keywords from Photos.app that
#       have not been assigned to any photos

# plst: -

# rslt: «list» : On successful completion, the script reacquires an updated
#                list of disused keywords and returns the result (hopefully
#                an empty list)
#       «err » : Script failure throws an error.  Running the script again
#                with Photos.app already open may yield a different result.
--------------------------------------------------------------------------------
# sown: CK
# ascd: 2019-01-07
# asmo: 2019-01-07
# vers: 1.0
--------------------------------------------------------------------------------
use sys : application "System Events"
use Photos : application "Photos"

property process : a reference to application process "Photos"

property _M : a reference to every media item
--------------------------------------------------------------------------------
# IMPLEMENTATION:
activate Photos
open the keywordManager

set everyKeyword to the list of allKeywords()
set activeKeywords to the list of currentKeywords()
set disusedKeywords to difference(everyKeyword, activeKeywords)

tell the keywordManager
    tell its keywordEditor
        open it
        select disusedKeywords
        delete
        close
    end tell
end tell

set everyKeyword to the list of allKeywords()
set activeKeywords to the list of currentKeywords()

close the keywordManager

set disusedKeywords to difference(everyKeyword, activeKeywords)
--------------------------------------------------------------------------------
# HANDLERS & SCRIPT OBJECTS:
script keywordManager
    property window : a reference to window "Keywords" of my process
    property scroll area : a reference to scroll area 2 of my window
    property button : a reference to button "Edit Keywords" of my window
    property menu item : a reference to ¬
        (menu item "Keyword Manager" of ¬
            menu 1 of ¬
            menu bar item "Window" of ¬
            menu bar 1 of my process)

    script keywordEditor
        property title : "Manage My Keywords"
        property window : a reference to window title of my process
        property scroll area : a reference to scroll area 1 ¬
            of my window
        property table : a reference to table 1 of my scroll area
        property group : a reference to group 1 of my window
        property button : a reference to (first button of my group ¬
            whose accessibility description = "remove")
        property menu item : a reference to ¬
            (menu item "Select All" of ¬
                menu 1 of ¬
                menu bar item "Edit" of ¬
                menu bar 1 of my process)
        to open
            if my window exists then return
            tell the keywordManager to if not ¬
                (its window exists) then ¬
                open it

            click the keywordManager's button
            with timeout of 10 seconds
                repeat until the my window exists
                    delay 0.5
                end repeat
            end timeout
            perform action "AXRaise" of my window
        end open

        to close
            if not (my window exists) then return
            click button "OK" of my window
        end close

        to select |keywords| as list
            local |keywords|

            set focused of my table to true

            click my menu item

            script deselect
                property list : |keywords|
                on fn(x)
                    if the value of x's text field 1 ¬
                        is not in my list then
                        set x's selected to false
                        return true
                    end if
                    false
                end fn
            end script

            filterItems from rows of my table ¬
                given handler:deselect
        end select

        to delete
            if not (my button exists) then return 0
            click my button
        end delete
    end script

    on menuItem()
        tell my menu item to if exists then return it
        false
    end menuItem

    to open
        if my window exists then return false
        tell the keywordEditor to if ¬
            (its window exists) then ¬
            return close it

        click my menuItem()

        # tell sys to keystroke "k" using command down

        with timeout of 10 seconds
            repeat until my window exists
                delay 0.5
                set my process's frontmost to true
            end repeat
        end timeout
        perform action "AXRaise" of my window
    end open

    to close
        if not (my window exists) then return
        click (value of attribute "AXCloseButton" of my window)
    end close
end script

on allKeywords()
    script |keywords|
        property list : accessibility description of ¬
            every checkbox of the keywordManager's scroll area ¬
            whose role description = "keyword checkbox"
    end script
end allKeywords

on currentKeywords()
    script
        property keep : keywords of _M
        property list : strings in unique_(flatten_(keep))
    end script
end currentKeywords

on __(function)
    if the function's class = script ¬
        then return the function

    script
        property fn : function
    end script
end __

to filterItems from L as list into R as list : missing value ¬
    given handler:function
    local L, R

    if R = missing value then set R to {}

    script
        property list : L
        property result : R
    end script

    tell the result to repeat with x in its list
        if __(function)'s fn(x, its list, its result) ¬
            then set end of its result to x's contents
    end repeat

    R
end filterItems

to foldItems from L at |ξ| : 0 given handler:function
    local L, |ξ|, function

    script
        property list : L
    end script

    tell the result to repeat with i from 1 to length of its list
        set x to item i in its list
        tell __(function)'s fn(x, |ξ|, i, L) to ¬
            if it = missing value then
                exit repeat
            else
                set |ξ| to it
            end if
    end repeat

    |ξ|
end foldItems

on difference(A as list, B as list)
    local A, B

    script
        on notMember(M)
            script
                on fn(x)
                    x is not in M
                end fn
            end script
        end notMember
    end script

    filterItems from A given handler:result's notMember(B)
end difference

on union(A as list, B as list)
    local A, B

    script
        on insert(x, L)
            set end of L to x
            L
        end insert
    end script

    foldItems from A at B given handler:result's insert
end union

to flatten:L
    foldItems from L at {} given handler:union
end flatten:

on unique:L
    local L

    script
        on notMember(x, i, L)
            x is not in L
        end notMember
    end script

    filterItems from L given handler:result's notMember
end unique:
---------------------------------------------------------------------------❮END

Niepewności dotyczące zagrożeń dla wydajności

  1. Jeśli chodzi o ilość zdjęć w bibliotece , najbardziej pamiętam o następującej linii:

    property _M : a reference to every media item

    których efekt wejdzie w grę w punktach skryptu, w których właściwość jest wyłuskiwana, tj

    set activeKeywords to the list of currentKeywords()

    Funkcja tego wiersza polega na pobraniu listy wszystkich słów kluczowych aktualnie przypisanych do co najmniej jednego zdjęcia. W tym celu należy wyliczyć (pobrać) każde zdjęcie w bibliotece i keywordsocenić jego właściwość. Dzieje się to praktycznie natychmiast na początku skryptu; i ponownie po usunięciu słów kluczowych w celu ustalenia, czy czyszczenie zostało zakończone. Jest to proces czasochłonny, a zatem potencjalne zagrożenie przekroczeniem limitu czasu przez skrypt.

    Powinno być możliwe przedłużenie domyślnej wartości limitu czasu w następujący sposób: z limitem 600 sekund ustaw wartość activeKe words na listę bieżących limitów czasu zakończenia

    lub może być konieczna nieznaczna zmiana składni przy pobieraniu zdjęć, tak aby skrypt bezpośrednio celował w aplikację Zdjęcia w punkcie wyliczenia, zamiast poprzez odwołania do właściwości; a następnie ująć polecenie Zdjęcia w timeoutbloku. Ale na razie zostawiłem to, aby sprawdzić, czy skrypt będzie działał w twoim systemie z domyślnym limitem czasu, co może nie być ograniczeniem, jeśli wyliczenie odbywa się synchronicznie (i nie wiem, czy to robi) .

  2. Jeśli chodzi o potencjalne blokady skryptów interfejsu użytkownika: słownik AppleScript ze zdjęciami nie zapewnia sposobu na odzyskanie wszystkich słów kluczowych istniejących w aplikacji. Sposób działania tego skryptu polega na otwarciu Menedżera słów kluczowych i odczytaniu nazwy każdej etykiety słowa kluczowego wykrytej w sekcji "Keywords". Nie wiem, czy każdy element interfejsu użytkownika zawierający etykietę słowa kluczowego jest ładowany podczas tworzenia okna Menedżera słów kluczowych ; lub czy zostaną one załadowane fragmentarycznie, gdy użytkownik przewinie listę. Ta ostatnia sytuacja byłaby uciążliwa, ponieważ spowodowałaby niepełną listę słów kluczowych, a następnie niepełne oczyszczenie.

    Jednym oczywistym rozwiązaniem byłoby uruchomienie skryptu wiele razy, aby wykonać wiele czystek, dopóki nie pozostaną żadne usuwalne elementy.

  3. Biorąc pod uwagę najgorszy scenariusz , analiza skryptu wydaje się mieć jeden z trzech możliwych wyników (niezależnie od tego, jak skrypt się zakończy, czy to przez zakończenie jego uruchomienia, czy przez zgłoszenie błędu):

    • Albo skrypt nic nie robi (wynik zerowy);
    • LUB następuje niepełne oczyszczenie (częściowy sukces);
    • LUB nastąpi całkowite oczyszczenie (sukces).

    Wydaje się, że nie ma sposobu na awarię skryptu w sposób, który negatywnie wpłynie na bibliotekę Zdjęć , więc najgorszym scenariuszem wydaje się być zero . Jeśli jednak założymy, że mogę się mylić, możesz zaryzykować błąd w potencjalnym gorszym scenariuszu.

    Ten margines zależy od ciebie i twojego osądu, co jest trudne do ustalenia, gdy być może nie wiesz, o jakich typach rzeczy zwykle się mylę. Jeśli to pomoże, powiedziałbym, że etykieta nie może usunąć żadnego z twoich zdjęć, ponieważ nie wykonuje żadnych operacji na systemie plików. Jeśli niemożliwe nie jest wystarczające, wyraźnym środkiem ostrożności jest wcześniejsze wykonanie kopii zapasowej całej biblioteki zdjęć . W zależności od wielkości twojej biblioteki, może to wahać się od prostego do nakładania bólu ze względu na niemal zerową szansę.

    Oczywiście skrypt wykonuje (oczywiście) czytanie i edycję list słów kluczowych. Więc chociaż nie powinno to być możliwe, nie byłoby głupotą uważać, że wszystkie słowa kluczowe dla wszystkich zdjęć mogą po prostu zniknąć. Jeśli chcesz zabezpieczyć się przed tym mało prawdopodobnym wydarzeniem, zapewniam ten „skryptlet”, który możesz uruchomić wcześniej, aby utworzyć kopię zapasową słów kluczowych:

    property path : "~/Desktop/Photos.Keywords.Backup.plist"
    
    
    backupKeywordsToFile at path
    --! CAUTION: Uncommenting the line below
    --! WILL OVERWRITE ALL KEYWORDS FOR ALL PHOTOS 
    -- restoreKeywordsFromFile at path
    --------------------------------------------------------------------------------
    # HANDLERS & SCRIPT OBJECTS:
    use framework "Foundation"
    
    property this : a reference to current application
    property _0 : a reference to missing value
    property _1 : a reference to reference
    
    property NSDictionary : a reference to NSDictionary of this
    property NSString : a reference to NSString of this
    property NSURL : a reference to NSURL of this
    
    to backupKeywordsToFile at fp as text
        local fp
    
        set fURL to NSURL's fileURLWithPath:((NSString's ¬
            stringWithString:fp)'s ¬
            stringByStandardizingPath())
    
        script
            use application "Photos"
            property _M : a reference to media items
            property properties : [keywords, id] of _M
            property keys : item 1 of my properties
            property refs : item 2 of my properties
        end script
    
        tell the result
            repeat with i from 1 to length of its keys
                if (item i of its keys) = missing value ¬
                    then set item i of its keys to {}
            end repeat
    
            tell (NSDictionary's dictionaryWithObjects:(its keys) ¬
                forKeys:(its refs)) to set [success, E] ¬
                to its writeToURL:fURL |error|:_1
        end tell
    
        if E  missing value then return E's localizedDescription() as text
    
        success
    end backupKeywordsToFile
    
    to restoreKeywordsFromFile at fp as text
        local fp
    
        set fURL to NSURL's fileURLWithPath:((NSString's ¬
            stringWithString:fp)'s ¬
            stringByStandardizingPath())
    
        script
            property result : NSDictionary's ¬
                dictionaryWithContentsOfURL:fURL ¬
                    |error|:_1
            property mediakeys : item 1 of my result
            property E : item 2 of my result
            property keys : null
            property refs : null
        end script
    
        tell the result
            if its E ≠ missing value then return its E's ¬
                localizedDescription() as text
    
            set its keys to its mediakeys's allObjects() as list
            set its refs to its mediakeys's allKeys() as list
    
            repeat with i from 1 to length of its refs
                set x to item i of its refs
                set keys to item i of its keys
    
                tell application "Photos" to set ¬
                    keywords of media item id x ¬
                    to keys
            end repeat
        end tell
    end restoreKeywordsFromFile

    Podczas tworzenia kopii zapasowej słów kluczowych skrypt będzie musiał wyliczyć całą bibliotekę Zdjęć . Dlatego bez względu na to, czy potrzebujesz kopii zapasowej, najpierw uruchom ten skrypt, aby wskazać, jak wolno / szybko można odczytać bibliotekę.

CJK
źródło
To całkiem niesamowite. Najpierw uruchomiłem drugi skrypt, aby był bezpieczny i działał przez kilka minut, a potem zmarł o error "Internal table overflow." number -2707godzinie (makeJSONString of me to fp given data:it). Myślę, że ma to związek z faktem, że mam setki słów kluczowych i ponad 50 000 zdjęć.
Dave Sag
Tak, głośność twojej biblioteki brzmi jak prawdopodobna przyczyna. 50 000 zdjęć to dużo, aby skrypt mógł wyliczyć je jako kolekcję za jednym razem, ale może być możliwe (choć wolniej) iterowanie listy i wyszukiwanie słów kluczowych pojedynczo.
CJK
1
Przed wypróbowaniem indywidualnej metody iteracji postanowiłem sprawdzić, czy użycie innego typu danych dla pliku byłoby pomocne. Zaktualizowałem drugi skrypt, aby używał danych listy właściwości zamiast JSON. W mojej krótkiej symulacji stworzyłem kolekcję danych, która zawierałaby 50 000 kluczy sparowanych z 50 000 wartości, i odtworzyła "Internal table overflow."błąd przy próbie konwersji na ciąg JSON. Jednak z tą samą kolekcją został przekonwertowany na listę właściwości i zapisany do pliku pomyślnie. Byłbym wdzięczny za wypróbowanie nowego skryptu kopii zapasowej i zgłoszenie się.
CJK
to działało i dość szybko.
Dave Sag
1
@DaveSag Witam, przepraszam, że zniknąłem - problemy zdrowotne znów się pojawiły. Co było najnowsze w odniesieniu do tego problemu? Przyszło mi do głowy, że ponieważ skrypt używany do tworzenia kopii zapasowych słów kluczowych przy użyciu danych z listy właściwości działa z dużym zbiorem danych, pierwotny zamierzony skrypt można prawdopodobnie odrzucić, ponieważ skrypt kopii zapasowej ma również moduł obsługi restoreKeywordsFromFile. Dlatego najprostszą metodą może być utworzenie kopii zapasowej słów kluczowych; ręcznie usuń wszystkie słowa kluczowe z Photos.app ; następnie przywróć słowa kluczowe z pliku kopii zapasowej.
CJK