safari_bookmarks_parser 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +28 -0
- data/lib/safari_bookmarks_parser.rb +6 -0
- data/lib/safari_bookmarks_parser/bookmark_folder.rb +4 -3
- data/lib/safari_bookmarks_parser/commands/base_command.rb +64 -0
- data/lib/safari_bookmarks_parser/commands/dump_command.rb +11 -61
- data/lib/safari_bookmarks_parser/commands/dups_command.rb +64 -0
- data/lib/safari_bookmarks_parser/commands/empty_command.rb +34 -0
- data/lib/safari_bookmarks_parser/parser.rb +14 -1
- data/lib/safari_bookmarks_parser/runner.rb +2 -0
- data/lib/safari_bookmarks_parser/services/find_duplicated_bookmarks.rb +18 -0
- data/lib/safari_bookmarks_parser/services/find_empty_folders.rb +30 -0
- data/lib/safari_bookmarks_parser/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64c367d9f7044efc558a5aeec069249c6152edfd35081e8cb1bb718bd0531c7c
|
4
|
+
data.tar.gz: 0c65ce14b8b0fb6705dec8eb61d0ac61ede0c039c0873af830fac68becb5cbdd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39daa035d8e210d82ff706e229fee99f4ca705112275c5cc1b5517c1f30e07c67a39da0288cdc3a6bc48cec5ef9cd5f7395b96ee14a7c703dc603493fd1666d1
|
7
|
+
data.tar.gz: 0e81f0eb742de04011d38a42cc25d273d05775478a2a065ea0a190efad67d084724b148f684dab4c47e805feea10740849cacb1af2446270487138deb5616cb3
|
data/README.md
CHANGED
@@ -22,6 +22,12 @@ Dump `Bookmarks.plist`:
|
|
22
22
|
$ safari_bookmarks_parser dump
|
23
23
|
```
|
24
24
|
|
25
|
+
Dump `Bookmarks.plist` to `Bookmarks.json':
|
26
|
+
|
27
|
+
```
|
28
|
+
$ safari_bookmarks_parser dump -o Bookmarks.json
|
29
|
+
```
|
30
|
+
|
25
31
|
Dump `Bookmarks.plist` as list:
|
26
32
|
|
27
33
|
```
|
@@ -52,6 +58,28 @@ Dump other `Bookmarks.plist`:
|
|
52
58
|
$ safari_bookmarks_parser dump /path/to/Bookmarks.plist
|
53
59
|
```
|
54
60
|
|
61
|
+
### Dups
|
62
|
+
|
63
|
+
Find duplicated bookmarks:
|
64
|
+
|
65
|
+
```
|
66
|
+
$ safari_bookmarks_parser dups
|
67
|
+
```
|
68
|
+
|
69
|
+
Find duplicated bookmarks excluding reading list:
|
70
|
+
|
71
|
+
```
|
72
|
+
$ safari_bookmarks_parser dups -R
|
73
|
+
```
|
74
|
+
|
75
|
+
### Empty
|
76
|
+
|
77
|
+
Find empty folders:
|
78
|
+
|
79
|
+
```
|
80
|
+
$ safari_bookmarks_parser empty
|
81
|
+
```
|
82
|
+
|
55
83
|
## Development
|
56
84
|
|
57
85
|
- Run `bin/rubocop` to check syntax
|
@@ -5,9 +5,15 @@ require 'safari_bookmarks_parser/bookmark_folder'
|
|
5
5
|
|
6
6
|
require 'safari_bookmarks_parser/parser'
|
7
7
|
|
8
|
+
require 'safari_bookmarks_parser/services/find_duplicated_bookmarks'
|
9
|
+
require 'safari_bookmarks_parser/services/find_empty_folders'
|
10
|
+
|
8
11
|
require 'safari_bookmarks_parser/runner'
|
9
12
|
|
13
|
+
require 'safari_bookmarks_parser/commands/base_command'
|
10
14
|
require 'safari_bookmarks_parser/commands/dump_command'
|
15
|
+
require 'safari_bookmarks_parser/commands/dups_command'
|
16
|
+
require 'safari_bookmarks_parser/commands/empty_command'
|
11
17
|
|
12
18
|
require 'safari_bookmarks_parser/version'
|
13
19
|
|
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
module SafariBookmarksParser
|
4
4
|
class BookmarkFolder
|
5
|
-
attr_reader :title, :children
|
5
|
+
attr_reader :title, :folder_names, :children
|
6
6
|
|
7
|
-
def initialize(title:, children: [])
|
7
|
+
def initialize(title:, folder_names:, children: [])
|
8
8
|
@title = title
|
9
|
+
@folder_names = folder_names
|
9
10
|
@children = children
|
10
11
|
end
|
11
12
|
|
@@ -20,7 +21,7 @@ module SafariBookmarksParser
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def to_h
|
23
|
-
{ 'title' => title, 'children' => children.map(&:to_h) }
|
24
|
+
{ 'title' => title, 'folder_names' => folder_names, 'children' => children.map(&:to_h) }
|
24
25
|
end
|
25
26
|
|
26
27
|
def to_json(options)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module SafariBookmarksParser
|
8
|
+
module Commands
|
9
|
+
class BaseCommand
|
10
|
+
attr_reader :plist_path, :output_path, :output_format
|
11
|
+
|
12
|
+
def initialize(argv)
|
13
|
+
@plist_path = File.expand_path('~/Library/Safari/Bookmarks.plist')
|
14
|
+
@output_path = nil
|
15
|
+
@output_format = :json
|
16
|
+
|
17
|
+
parse_options(argv)
|
18
|
+
handle_argv(argv)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def format_to_text(obj)
|
24
|
+
case @output_format
|
25
|
+
when :json
|
26
|
+
JSON.pretty_generate(obj)
|
27
|
+
when :yaml
|
28
|
+
YAML.dump(obj)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def output_text(text)
|
33
|
+
if @output_path
|
34
|
+
File.write(@output_path, text)
|
35
|
+
else
|
36
|
+
puts text
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_output_path(parser)
|
41
|
+
parser.on('-o', '--output-path=PATH', 'Output path (default: output to $stdout)') do |value|
|
42
|
+
@output_path = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_output_format(parser)
|
47
|
+
desc = %(Output format (default: "#{@output_format}"; one of "json" or "yaml"))
|
48
|
+
parser.on('-f', '--output-format=FORMAT', %w[json yaml], desc) do |value|
|
49
|
+
@output_format = value.to_sym
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def do_parse(parser, argv)
|
54
|
+
parser.parse!(argv)
|
55
|
+
rescue OptionParser::ParseError => e
|
56
|
+
raise Error, e.message
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_argv(argv)
|
60
|
+
@plist_path = argv.first if argv.any?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,41 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'json'
|
4
|
-
require 'optparse'
|
5
|
-
require 'yaml'
|
6
|
-
|
7
3
|
module SafariBookmarksParser
|
8
4
|
module Commands
|
9
|
-
class DumpCommand
|
10
|
-
attr_reader :
|
5
|
+
class DumpCommand < BaseCommand
|
6
|
+
attr_reader :output_style, :output_parts
|
11
7
|
|
12
8
|
def initialize(argv)
|
13
|
-
@plist_path = File.expand_path('~/Library/Safari/Bookmarks.plist')
|
14
|
-
@output_path = nil
|
15
|
-
@output_format = :json
|
16
9
|
@output_style = :tree
|
17
10
|
@output_parts = :all
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
parse_options(argv)
|
22
|
-
handle_argv(argv)
|
12
|
+
super
|
23
13
|
end
|
24
14
|
|
25
15
|
def run
|
26
16
|
plist_parser = Parser.parse(@plist_path)
|
27
17
|
|
28
|
-
result =
|
29
|
-
result =
|
18
|
+
result = select_output_parts(plist_parser)
|
19
|
+
result = select_output_style(result)
|
30
20
|
|
31
|
-
text =
|
21
|
+
text = format_to_text(result)
|
32
22
|
|
33
23
|
output_text(text)
|
34
24
|
end
|
35
25
|
|
36
26
|
private
|
37
27
|
|
38
|
-
def
|
28
|
+
def select_output_parts(plist_parser)
|
39
29
|
case @output_parts
|
40
30
|
when :all
|
41
31
|
plist_parser.root_folder
|
@@ -46,7 +36,7 @@ module SafariBookmarksParser
|
|
46
36
|
end
|
47
37
|
end
|
48
38
|
|
49
|
-
def
|
39
|
+
def select_output_style(result)
|
50
40
|
case @output_style
|
51
41
|
when :tree
|
52
42
|
result.to_h
|
@@ -55,23 +45,6 @@ module SafariBookmarksParser
|
|
55
45
|
end
|
56
46
|
end
|
57
47
|
|
58
|
-
def format_result(result)
|
59
|
-
case @output_format
|
60
|
-
when :json
|
61
|
-
JSON.pretty_generate(result)
|
62
|
-
when :yaml
|
63
|
-
YAML.dump(result)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def output_text(text)
|
68
|
-
if @output_path
|
69
|
-
File.write(@output_path, text)
|
70
|
-
else
|
71
|
-
puts text
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
48
|
def parse_options(argv)
|
76
49
|
parser = OptionParser.new
|
77
50
|
|
@@ -84,24 +57,11 @@ module SafariBookmarksParser
|
|
84
57
|
on_list(parser)
|
85
58
|
|
86
59
|
on_reading_list_only(parser)
|
87
|
-
|
60
|
+
on_exclude_reading_list(parser)
|
88
61
|
|
89
62
|
do_parse(parser, argv)
|
90
63
|
end
|
91
64
|
|
92
|
-
def on_output_path(parser)
|
93
|
-
parser.on('-o', '--output-path=PATH', 'Output path (default: output to $stdout)') do |value|
|
94
|
-
@output_path = value
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def on_output_format(parser)
|
99
|
-
desc = "Output format (default: #{@output_format}; one of json or yaml)"
|
100
|
-
parser.on('-f', '--output-format=FORMAT', %w[json yaml], desc) do |value|
|
101
|
-
@output_format = value.to_sym
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
65
|
def on_tree(parser)
|
106
66
|
parser.on('--tree', 'Output as tree (default)') do
|
107
67
|
@output_style = :tree
|
@@ -120,22 +80,12 @@ module SafariBookmarksParser
|
|
120
80
|
end
|
121
81
|
end
|
122
82
|
|
123
|
-
def
|
124
|
-
parser.on('-R', '
|
83
|
+
def on_exclude_reading_list(parser)
|
84
|
+
parser.on('-R', 'Exclude reading list') do
|
125
85
|
@output_parts = :bookmarks
|
126
86
|
end
|
127
87
|
end
|
128
88
|
|
129
|
-
def do_parse(parser, argv)
|
130
|
-
parser.parse!(argv)
|
131
|
-
rescue OptionParser::ParseError => e
|
132
|
-
raise Error, e.message
|
133
|
-
end
|
134
|
-
|
135
|
-
def handle_argv(argv)
|
136
|
-
@plist_path = argv.first if argv.any?
|
137
|
-
end
|
138
|
-
|
139
89
|
Runner.register_command(:dump, self)
|
140
90
|
end
|
141
91
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafariBookmarksParser
|
4
|
+
module Commands
|
5
|
+
class DupsCommand < BaseCommand
|
6
|
+
attr_reader :exclude_reading_list
|
7
|
+
|
8
|
+
def initialize(argv)
|
9
|
+
@exclude_reading_list = false
|
10
|
+
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
plist_parser = Parser.parse(@plist_path)
|
16
|
+
|
17
|
+
bookmarks = bookmarks_to_process(plist_parser).to_a
|
18
|
+
|
19
|
+
duplicated_bookmark_groups = Services::FindDuplicatedBookmarks.call(bookmarks: bookmarks)
|
20
|
+
|
21
|
+
return if duplicated_bookmark_groups.empty?
|
22
|
+
|
23
|
+
duplicated_bookmark_groups = duplicated_bookmark_groups.map do |group|
|
24
|
+
group.map(&:to_h)
|
25
|
+
end
|
26
|
+
|
27
|
+
text = format_to_text(duplicated_bookmark_groups)
|
28
|
+
|
29
|
+
output_text(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def bookmarks_to_process(plist_parser)
|
35
|
+
if @exclude_reading_list
|
36
|
+
plist_parser.root_folder_without_reading_list
|
37
|
+
else
|
38
|
+
plist_parser.root_folder
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_options(argv)
|
43
|
+
parser = OptionParser.new
|
44
|
+
|
45
|
+
parser.banner = "Usage: #{parser.program_name} dups [options] [~/Library/Safari/Bookmarks.plist]"
|
46
|
+
|
47
|
+
on_output_path(parser)
|
48
|
+
on_output_format(parser)
|
49
|
+
|
50
|
+
on_exclude_reading_list(parser)
|
51
|
+
|
52
|
+
do_parse(parser, argv)
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_exclude_reading_list(parser)
|
56
|
+
parser.on('-R', "Exclude reading list (default: #{@exclude_reading_list})") do
|
57
|
+
@exclude_reading_list = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Runner.register_command(:dups, self)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafariBookmarksParser
|
4
|
+
module Commands
|
5
|
+
class EmptyCommand < BaseCommand
|
6
|
+
def run
|
7
|
+
plist_parser = Parser.parse(@plist_path)
|
8
|
+
|
9
|
+
empty_folders = Services::FindEmptyFolders.call(root_folder: plist_parser.root_folder)
|
10
|
+
|
11
|
+
return if empty_folders.empty?
|
12
|
+
|
13
|
+
text = format_to_text(empty_folders.map(&:to_h))
|
14
|
+
|
15
|
+
output_text(text)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_options(argv)
|
21
|
+
parser = OptionParser.new
|
22
|
+
|
23
|
+
parser.banner = "Usage: #{parser.program_name} empty [options] [~/Library/Safari/Bookmarks.plist]"
|
24
|
+
|
25
|
+
on_output_path(parser)
|
26
|
+
on_output_format(parser)
|
27
|
+
|
28
|
+
do_parse(parser, argv)
|
29
|
+
end
|
30
|
+
|
31
|
+
Runner.register_command(:empty, self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -65,7 +65,7 @@ module SafariBookmarksParser
|
|
65
65
|
traverse(child, folder_names + [title])
|
66
66
|
end.compact
|
67
67
|
|
68
|
-
BookmarkFolder.new(title: title, children: children)
|
68
|
+
BookmarkFolder.new(title: title, folder_names: folder_names, children: children)
|
69
69
|
end
|
70
70
|
|
71
71
|
def accept_leaf(node, folder_names)
|
@@ -80,6 +80,8 @@ module SafariBookmarksParser
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def binary_plist_to_xml_plist(binary_plist_path)
|
83
|
+
binary_plist_readable?(binary_plist_path)
|
84
|
+
|
83
85
|
Tempfile.open(['Bookmarks', '.xml']) do |tempfile|
|
84
86
|
command = ['plutil', '-convert', 'xml1', '-o', tempfile.path, binary_plist_path]
|
85
87
|
|
@@ -88,5 +90,16 @@ module SafariBookmarksParser
|
|
88
90
|
tempfile.read
|
89
91
|
end
|
90
92
|
end
|
93
|
+
|
94
|
+
def binary_plist_readable?(binary_plist_path)
|
95
|
+
return if File.readable?(binary_plist_path)
|
96
|
+
|
97
|
+
message = <<~MESSAGE
|
98
|
+
Couldn't read #{binary_plist_path}
|
99
|
+
In "System Preferences" -> "Security & Privacy" -> "Privacy" -> "Full Disk Access", check "Terminal".
|
100
|
+
MESSAGE
|
101
|
+
|
102
|
+
raise Error, message
|
103
|
+
end
|
91
104
|
end
|
92
105
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafariBookmarksParser
|
4
|
+
module Services
|
5
|
+
class FindDuplicatedBookmarks
|
6
|
+
def self.call(bookmarks:)
|
7
|
+
cache = Hash.new {|hash, key| hash[key] = [] }
|
8
|
+
|
9
|
+
bookmarks.to_a.each do |bookmark|
|
10
|
+
key = bookmark.url.sub(%r{\Ahttps?://}, '')
|
11
|
+
cache[key] << bookmark
|
12
|
+
end
|
13
|
+
|
14
|
+
cache.select {|_, value| value.size > 1 }.values
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafariBookmarksParser
|
4
|
+
module Services
|
5
|
+
class FindEmptyFolders
|
6
|
+
class << self
|
7
|
+
def call(root_folder:)
|
8
|
+
results = []
|
9
|
+
|
10
|
+
traverse(root_folder, results)
|
11
|
+
|
12
|
+
results
|
13
|
+
end
|
14
|
+
|
15
|
+
def traverse(node, results)
|
16
|
+
case node
|
17
|
+
when BookmarkFolder
|
18
|
+
if node.children.empty?
|
19
|
+
results << node
|
20
|
+
else
|
21
|
+
node.children.each do |child|
|
22
|
+
traverse(child, results)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safari_bookmarks_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- healthypackrat
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: plist
|
@@ -38,9 +38,14 @@ files:
|
|
38
38
|
- lib/safari_bookmarks_parser.rb
|
39
39
|
- lib/safari_bookmarks_parser/bookmark.rb
|
40
40
|
- lib/safari_bookmarks_parser/bookmark_folder.rb
|
41
|
+
- lib/safari_bookmarks_parser/commands/base_command.rb
|
41
42
|
- lib/safari_bookmarks_parser/commands/dump_command.rb
|
43
|
+
- lib/safari_bookmarks_parser/commands/dups_command.rb
|
44
|
+
- lib/safari_bookmarks_parser/commands/empty_command.rb
|
42
45
|
- lib/safari_bookmarks_parser/parser.rb
|
43
46
|
- lib/safari_bookmarks_parser/runner.rb
|
47
|
+
- lib/safari_bookmarks_parser/services/find_duplicated_bookmarks.rb
|
48
|
+
- lib/safari_bookmarks_parser/services/find_empty_folders.rb
|
44
49
|
- lib/safari_bookmarks_parser/version.rb
|
45
50
|
homepage: https://github.com/healthypackrat/safari_bookmarks_parser
|
46
51
|
licenses:
|