cocoapods-yaml 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ require 'json'
2
+ require 'shellwords'
3
+ require 'tempfile'
4
+
5
+ module Xcodeproj
6
+
7
+ # 模拟器架构兼容检查:扫描已安装的二进制依赖(xcframework / fat framework / 静态库.a),
8
+ # 找出模拟器产物不含 arm64 slice 的组件。Apple Silicon 上 Xcode 默认按 arm64 编译模拟器
9
+ # 目标,这类组件无法参与链接(报 framework not found),只能 Rosetta 运行或重新打包支持。
10
+ # 源码编译型组件(无预编译二进制)天然支持,不在检查范围
11
+ class SimulatorArchChecker
12
+
13
+ # Pods目录下非组件安装目录(跳过不扫描)
14
+ SKIP_PODS_DIR_NAMES = ['Headers', 'Local Podspecs', 'Manifest.lock', 'Pods.xcodeproj', 'Target Support Files'].freeze
15
+
16
+ def check
17
+ pods_dir = File.join(Dir.pwd, 'Pods')
18
+ unless File.directory?(pods_dir)
19
+ return
20
+ end
21
+
22
+ log.step('模拟器架构检查中...')
23
+ start = (Time.now.to_f * 1000).to_i
24
+
25
+ rosetta = []
26
+ no_sim = []
27
+ checked = 0
28
+
29
+ Dir.children(pods_dir).sort.each do |name|
30
+ next if SKIP_PODS_DIR_NAMES.include?(name)
31
+ pod_dir = File.join(pods_dir, name)
32
+ next unless File.directory?(pod_dir)
33
+
34
+ status = check_pod_dir(pod_dir)
35
+ next if status == :none
36
+ checked += 1
37
+ if status == :rosetta
38
+ rosetta << name
39
+ elsif status == :no_sim
40
+ no_sim << name
41
+ end
42
+ end
43
+
44
+ if rosetta.any?
45
+ log.warn('以下组件的模拟器产物不含 arm64 slice,Apple Silicon 上按默认 arm64 编译模拟器会链接失败(报 framework not found):')
46
+ log.warn(" #{rosetta.join('、')}")
47
+ log.warn('建议用打包工具重新发布这些组件以支持 arm64 模拟器;临时可在工程设置 EXCLUDED_ARCHS=arm64 用 Rosetta 模式运行')
48
+ end
49
+
50
+ if no_sim.any?
51
+ log.warn('以下组件完全不含模拟器 slice,无法在模拟器上编译链接(仅真机可用):')
52
+ log.warn(" #{no_sim.join('、')}")
53
+ end
54
+
55
+ duration = (((Time.now.to_f * 1000).to_i - start) * 0.001).round(2)
56
+ log.info("模拟器架构检查完毕! 共检查#{checked}个含二进制产物的组件(不支持arm64模拟器 #{rosetta.size}个) 耗时:#{duration}秒")
57
+ end
58
+
59
+ private
60
+
61
+ def log
62
+ CocoaPods::Yaml.log
63
+ end
64
+
65
+ # @return [:ok, :rosetta, :no_sim, :none] 组件内预编译二进制的模拟器支持情况(取最差结果);
66
+ # :none 表示目录内没有预编译二进制(源码编译型组件)
67
+ def check_pod_dir(pod_dir)
68
+ worst = :none
69
+ each_binary(pod_dir) do |kind, path|
70
+ status = classify_binary(kind, path)
71
+ next if status == :none
72
+ worst = status if worst == :none || (worst == :rosetta && status == :no_sim)
73
+ end
74
+ worst
75
+ end
76
+
77
+ # 遍历组件目录内的预编译二进制:xcframework / fat framework / 静态库.a
78
+ def each_binary(pod_dir)
79
+ Dir.glob(File.join(pod_dir, '**', '*.xcframework')).sort.each do |path|
80
+ yield(:xcframework, path)
81
+ end
82
+ Dir.glob(File.join(pod_dir, '**', '*.framework')).sort.reject { |path| path.include?('.xcframework/') }.each do |framework|
83
+ binary = File.join(framework, File.basename(framework, '.framework'))
84
+ yield(:framework, binary) if File.file?(binary)
85
+ end
86
+ Dir.glob(File.join(pod_dir, '**', '*.a')).sort.reject { |path| path.include?('.xcframework/') }.each do |path|
87
+ yield(:static, path)
88
+ end
89
+ end
90
+
91
+ # @return [:ok, :rosetta, :no_sim, :none] :none表示非Mach-O或无Info.plist,跳过不计
92
+ def classify_binary(kind, path)
93
+ return xcframework_status(path) if kind == :xcframework
94
+
95
+ archs = `lipo -archs #{Shellwords.escape(path)} 2>/dev/null`.split
96
+ return :none if archs.empty?
97
+
98
+ if archs.include?('arm64')
99
+ return :ok if arm64_supports_simulator?(path)
100
+ return :rosetta if archs.include?('x86_64')
101
+ :no_sim
102
+ else
103
+ archs.include?('x86_64') ? :rosetta : :no_sim
104
+ end
105
+ end
106
+
107
+ # xcframework 的模拟器slice架构;Info.plist缺失或解析失败返回nil(跳过不计)
108
+ def xcframework_status(xcframework_path)
109
+ archs = xcframework_simulator_archs(xcframework_path)
110
+ return :none if archs.nil?
111
+ return :no_sim if archs.empty?
112
+ return :ok if archs.include?('arm64')
113
+ archs.include?('x86_64') ? :rosetta : :no_sim
114
+ end
115
+
116
+ # 解析xcframework的Info.plist,返回模拟器slice支持的架构数组(无模拟器slice返回空数组)
117
+ def xcframework_simulator_archs(xcframework_path)
118
+ plist_path = File.join(xcframework_path, 'Info.plist')
119
+ return nil unless File.exist?(plist_path)
120
+
121
+ json = `plutil -convert json -o - #{Shellwords.escape(plist_path)} 2>/dev/null`
122
+ plist = JSON.parse(json) rescue nil
123
+ libraries = plist.is_a?(Hash) ? plist['AvailableLibraries'] : nil
124
+ return nil unless libraries.is_a?(Array)
125
+
126
+ libraries.select { |lib| lib['SupportedPlatformVariant'] == 'simulator' }
127
+ .flat_map { |lib| Array(lib['SupportedArchitectures']) }
128
+ end
129
+
130
+ # arm64 slice 是否支持模拟器:现代 arm64-simulator 目标必带 LC_BUILD_VERSION 且
131
+ # platform 为 7(iOSSimulator);其余情况(LC_VERSION_MIN_IPHONEOS 或 platform=iOS)均为真机
132
+ def arm64_supports_simulator?(binary)
133
+ Tempfile.create(['arm64_slice', '.a']) do |tmp|
134
+ tmp.close
135
+ unless system("lipo #{Shellwords.escape(binary)} -thin arm64 -output #{Shellwords.escape(tmp.path)}",
136
+ out: File::NULL, err: File::NULL)
137
+ return false
138
+ end
139
+ load_info = `otool -l #{Shellwords.escape(tmp.path)} 2>/dev/null`
140
+ load_info.include?('LC_BUILD_VERSION') && load_info.match?(/\bplatform\s+7\b/)
141
+ end
142
+ end
143
+
144
+ end
145
+ end
@@ -0,0 +1,37 @@
1
+ module Xcodeproj
2
+ class Util
3
+ #读取podspec内容
4
+ # @return [Hash]
5
+ # @param podspec_path::[Pathname]
6
+ def self.get_podspec_content(podspec_path)
7
+
8
+ podspec_json_path = podspec_path.to_s.end_with?(".podspec.json") ? podspec_path : nil
9
+
10
+ podspec_content = nil
11
+ if podspec_json_path and File.exist?(podspec_json_path)
12
+ json = File.read(podspec_json_path)
13
+ podspec_content = JSON.parse(json)
14
+ elsif podspec_path and File.exist?(podspec_path)
15
+ json = get_podspec_file_content(podspec_path)
16
+ podspec_content = JSON.parse(json)
17
+ end
18
+ podspec_content
19
+ end
20
+
21
+ # 根据podspec路径把podspec转为json
22
+ def self.get_podspec_file_content(podspec_path)
23
+
24
+ spec_contents = File.open(podspec_path, 'r:utf-8', &:read)
25
+
26
+ if spec_contents.respond_to?(:encoding) && spec_contents.encoding.name != 'UTF-8'
27
+ spec_contents.encode!('UTF-8')
28
+ end
29
+
30
+ path = Pathname.new(podspec_path)
31
+
32
+ spec = ::Pod._eval_podspec(spec_contents, path)
33
+
34
+ spec.to_json
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,81 @@
1
+ require 'xcodeproj'
2
+ require 'pathname'
3
+
4
+ module XCProject
5
+
6
+ class XcodeprojManager
7
+
8
+ # 缓存某个的依赖库
9
+ # {{"module_nameA":["module_nameA", module_nameB]},}
10
+ attr_reader :module_frameworks
11
+
12
+ def initialize
13
+ @module_frameworks = {}
14
+ end
15
+
16
+ # 单例方法
17
+ @@instance = XcodeprojManager.new
18
+
19
+ # 获取单例
20
+ def self.share_manager
21
+ @@instance
22
+ end
23
+
24
+ private_class_method :new
25
+
26
+ # 壳工程xcodeproj文件名(取目录下第一个匹配项)
27
+ # @return [String, nil]
28
+ def get_shell_project_file
29
+ xcodeproj_file = nil
30
+ Dir.foreach(Dir.pwd) do |file|
31
+ if file.end_with?(".xcodeproj")
32
+ xcodeproj_file = file
33
+ break
34
+ end
35
+ end
36
+ xcodeproj_file
37
+ end
38
+
39
+ # 获取壳工程的project对象
40
+ def get_shell_project
41
+
42
+ xcodeproj_file = get_shell_project_file
43
+ return nil unless xcodeproj_file
44
+
45
+ xcodeproj_path = "#{Dir.pwd}/#{xcodeproj_file}"
46
+
47
+ Xcodeproj::Project.open(xcodeproj_path)
48
+
49
+ end
50
+
51
+ #获取主APP target列表 包含扩展
52
+ def get_shell_targets
53
+ results = []
54
+ project = self.get_shell_project
55
+ targets = project.root_object.targets
56
+ targets.each do |target|
57
+ results << target.name
58
+ end
59
+ results
60
+ end
61
+
62
+ #获取主工程全部顶层target(含App与扩展,是否可通过yaml集成由调用方按product_type过滤)
63
+ def get_app_targets
64
+ project = self.get_shell_project
65
+ project.root_object.targets
66
+ end
67
+
68
+ # 获取Pods下project对象
69
+ def get_pods_project
70
+
71
+ xcodeproj_path = "#{Dir.pwd}/Pods/Pods.xcodeproj"
72
+ unless File.exist?(xcodeproj_path)
73
+ return nil
74
+ end
75
+
76
+ Xcodeproj::Project.open(xcodeproj_path)
77
+
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,53 @@
1
+ require 'cocoapods'
2
+ require_relative '../variable'
3
+
4
+ module Pod
5
+ class Command
6
+ # 以 prepend 模块补丁的方式给官方 pod install / pod update 追加 --trim/--full 单次模式选项。
7
+ # 不 reopen 重写官方方法:类方法 options 与 initialize/run 均通过 super 走 CocoaPods 原始实现,
8
+ # 上游新增选项/参数校验时自动保留,不会随版本升级丢失。
9
+ # pod update 内部同样通过 Pod::Installer#install! 执行,下载流程补丁对其同样生效,
10
+ # 因此两个命令共享同一份开关解析。
11
+ module TrimModePatch
12
+ TRIM_OPTIONS = [
13
+ ['--trim', '单次开启未使用依赖裁剪(仅本次执行生效,优先于 LocalDependencies.yaml 的 TRIM_UNUSED_POD)'],
14
+ ['--full', '单次关闭未使用依赖裁剪,安装全部依赖(仅本次执行生效)']
15
+ ].freeze
16
+
17
+ module ClassMethods
18
+ def options
19
+ super.concat(TrimModePatch::TRIM_OPTIONS)
20
+ end
21
+ end
22
+
23
+ def self.prepended(base)
24
+ base.singleton_class.prepend(ClassMethods)
25
+ end
26
+
27
+ def initialize(argv)
28
+ @yaml_trim_mode = argv.flag?('trim')
29
+ @yaml_full_mode = argv.flag?('full')
30
+ # 此时 Podfile 尚未解析(在 run 阶段才读取),提前写入全局变量可供 yaml_dependencies! 使用
31
+ $trim_unused_pod = @yaml_trim_mode ? true : (@yaml_full_mode ? false : nil)
32
+ super
33
+ end
34
+
35
+ def run
36
+ # 参数互斥校验放在 run 而非 initialize:parse 实例化阶段抛出的异常
37
+ # 在 CLAide 中 command 尚为 nil,即使是 Informative 也会带堆栈输出
38
+ if @yaml_trim_mode && @yaml_full_mode
39
+ raise Pod::Informative, '--trim 与 --full 互斥,请只指定其中一个选项'
40
+ end
41
+ super
42
+ end
43
+ end
44
+
45
+ class Install < Command
46
+ prepend TrimModePatch
47
+ end
48
+
49
+ class Update < Command
50
+ prepend TrimModePatch
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ require 'cocoapods'
2
+ require_relative '../setup/podfile_setup'
3
+
4
+ module Pod
5
+ class Command
6
+ class Yaml < Command
7
+ self.abstract_command = true
8
+ self.summary = 'yaml依赖配置相关命令'
9
+
10
+ class Init < Yaml
11
+
12
+ self.summary = '生成接入cocoapods-yaml的模板Podfile'
13
+
14
+ self.description = <<-DESC
15
+ 在iOS工程根目录执行 pod yaml init:
16
+ * 有.xcodeproj且无Podfile: 直接生成模板Podfile及依赖yaml
17
+ * 已有Podfile但未接入yaml_dependencies!: 先备份为 Podfile.bak.时间戳 再生成模板
18
+ * 已接入: 不做处理
19
+ DESC
20
+
21
+ def run
22
+ CocoaPods::Yaml::PodfileSetup.run
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../dep/find'
2
+ require_relative '../../version'
3
+ module Pod
4
+ class Command
5
+ class Yaml < Command
6
+ class Relation < Yaml
7
+
8
+ self.summary = '从Podfile.lock检索组件的正向/反向依赖关系'
9
+
10
+ self.description = '默认输出反向依赖(谁依赖了该组件);加--forward输出正向依赖。' \
11
+ '不传组件名时输出全部组件,可配合--sort按依赖数量排序。'
12
+
13
+ self.arguments = [
14
+ CLAide::Argument.new('组件名(可选,不传则输出全部)', false)
15
+ ]
16
+
17
+ def self.options
18
+ [
19
+ %w[--sort 输出依赖时按数量从小到大排序],
20
+ %w[--forward 输出正向依赖],
21
+ ].concat(super).reject { |(name, _)| (name == '--no-ansi' or name == '--silent' or name == '--allow-root' or name == '--verbose') }
22
+ end
23
+
24
+ def initialize(argv)
25
+ @name = argv.shift_argument
26
+ super
27
+ CocoaPods::Yaml.log.info("插件版本号: #{CocoaPods::Yaml::VERSION}")
28
+ @sort_count = argv.flag?('sort', false)
29
+ @forward = argv.flag?('forward', false)
30
+ end
31
+
32
+ def run
33
+ verify_lockfile_exists!
34
+ finder = Pod::DepFinder.new
35
+ finder.find_name = @name
36
+ finder.sort_count = @sort_count
37
+ finder.forward = @forward
38
+ finder.find
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,164 @@
1
+ require 'yaml'
2
+ require 'colored2'
3
+
4
+ module Pod
5
+ class DepFinder
6
+
7
+ # @param [Hash] podfileLock内容
8
+ attr_accessor :lockfile_content
9
+
10
+ # @param [String] 要查询的组件名
11
+ attr_accessor :find_name
12
+
13
+ # @param [bool] 是否按数量从小到大排序
14
+ attr_accessor :sort_count
15
+
16
+ # @param [bool] 是否打印正向依赖
17
+ attr_accessor :forward
18
+
19
+ def initialize
20
+ super
21
+ init_lock
22
+ end
23
+
24
+ def init_lock
25
+ # 读取名为Podfile.lock的yaml文件, 并通过分析里面的PODS数组,输出依赖的第三方库的名字
26
+ unless File.exist?("Podfile.lock")
27
+ CocoaPods::Yaml.log.error("Podfile.lock文件不存在,请先执行pod install或pod update")
28
+ exit!
29
+ end
30
+
31
+ @sort_count = false
32
+
33
+ @lockfile_content = YAML.load_file('Podfile.lock')
34
+
35
+ end
36
+
37
+ # @api 反向依赖查询
38
+ def find
39
+
40
+ if @find_name
41
+ find_single_dep
42
+ else
43
+ find_all_dep
44
+ end
45
+
46
+ print_usage_hint
47
+ end
48
+
49
+ # 按当前场景只提示真正可用的后续操作
50
+ def print_usage_hint
51
+ hints = []
52
+ if @find_name
53
+ hints << '去掉 --forward 可同时看反向依赖' if @forward
54
+ else
55
+ hints << '查询单个组件: pod yaml relation <组件名>'
56
+ hints << '加 --forward 看正向依赖' unless @forward
57
+ hints << '加 --sort 按依赖数量从少到多排序' unless @sort_count
58
+ end
59
+ return if hints.empty?
60
+ CocoaPods::Yaml.log.info('')
61
+ CocoaPods::Yaml.log.detail(hints.join(';'))
62
+ end
63
+
64
+ def find_all_dep
65
+
66
+ result_map = Hash.new
67
+ # 循环遍历lockfile对象中的PODS数组
68
+ @lockfile_content['PODS'].each do |pod|
69
+ # 拿到所有的组件名
70
+ key = pod.is_a?(Hash) ? pod.keys[0] : pod
71
+ key = key.split(" (")[0]
72
+
73
+ if @forward
74
+ result_map[key] = pod.is_a?(Hash) ? pod.values[0] : Array.new
75
+ else
76
+ result_map[key] = find_reverse_with_name(key)
77
+ end
78
+ end
79
+
80
+ msg = @forward ? "正向依赖(各组件依赖了谁)" : "反向依赖(谁依赖了各组件)"
81
+ CocoaPods::Yaml.log.step(msg)
82
+
83
+ sorted_hash = result_map
84
+ if @sort_count
85
+ sorted_hash = Hash[result_map.sort_by { |key, value| value.length }]
86
+ end
87
+
88
+ sorted_hash.each do |key, array|
89
+ title = @forward ? "#{key} 依赖的组件(#{array.length}个)" : "依赖#{key}的组件(#{array.length}个)"
90
+ CocoaPods::Yaml.log.info(title, :green)
91
+ array.each { |obj| CocoaPods::Yaml.log.info(" #{obj}") }
92
+ CocoaPods::Yaml.log.info('')
93
+ end
94
+ end
95
+
96
+ def find_single_dep
97
+
98
+ CocoaPods::Yaml.log.step("#{@find_name} 依赖关系")
99
+
100
+ # 默认正反向都展示;--forward时只看正向
101
+ unless @forward
102
+ results = find_reverse_with_name(@find_name)
103
+ CocoaPods::Yaml.log.info("反向依赖 · 谁依赖了#{@find_name}(#{results.length}个):", :green)
104
+ results.each { |res| CocoaPods::Yaml.log.info(" #{res}") }
105
+ CocoaPods::Yaml.log.info('')
106
+ end
107
+
108
+ # 正向依赖查询结果
109
+ forward_results = find_forward_with_name(@find_name)
110
+ CocoaPods::Yaml.log.info("正向依赖 · #{@find_name}依赖了谁(#{forward_results.length}个):", :green)
111
+ forward_results.each { |res| CocoaPods::Yaml.log.info(" #{res}") }
112
+
113
+ end
114
+
115
+ def find_forward_with_name(name)
116
+ if not @lockfile_content or not @lockfile_content['PODS']
117
+ return []
118
+ end
119
+
120
+ results = Array.new
121
+ @lockfile_content['PODS'].each do |pod|
122
+ # 拿到所有的组件名
123
+ key = pod.is_a?(Hash) ? pod.keys[0] : pod
124
+ key = key.split(" (")[0]
125
+ if key == name and pod.is_a?(Hash)
126
+ pod.each_value do |value|
127
+ # value是依赖数组,需展平为逐条依赖,不能把整个数组当一个元素
128
+ results.concat(Array(value))
129
+ end
130
+ end
131
+ end
132
+ results
133
+ end
134
+
135
+ def find_reverse_with_name(name)
136
+ if not @lockfile_content or not @lockfile_content['PODS']
137
+ return []
138
+ end
139
+
140
+ results = Array.new
141
+ # 循环遍历lockfile对象中的PODS数组
142
+ @lockfile_content['PODS'].each do |pod|
143
+ if pod.is_a?(Hash)
144
+ pod.values.each do |value|
145
+ # 如果value是一个数组,则判断数组中是否包含name
146
+ if value.is_a?(Array)
147
+ value.each { |sub_value|
148
+ # 这里的处理包含了子模块的情况
149
+ sub_module = sub_value.split(" (")[0]
150
+ if sub_module == name
151
+ results << pod.keys.first
152
+ end
153
+ }
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ # 去重
160
+ results.uniq
161
+ end
162
+ end
163
+ end
164
+
@@ -0,0 +1,72 @@
1
+ require_relative '../podfilelock'
2
+
3
+ module PodfileLock
4
+ class Indirect
5
+ def log_indirect
6
+
7
+ # 内容读取
8
+ dependencies_lock = PodfileLock::PodfileLockManager.podfile_lock_content
9
+
10
+ dependencies = dependencies_lock['DEPENDENCIES']
11
+ unless dependencies
12
+ return
13
+ end
14
+
15
+ check_sums = dependencies_lock['SPEC CHECKSUMS']
16
+ unless check_sums
17
+ return
18
+ end
19
+
20
+ # 直接依赖名归一化(去掉版本要求和subspec)。
21
+ # 注意: 必须用非破坏式的map,不能对lock原始数组使用collect!,否则会改写内存中的lock数据
22
+ direct_dependencies = dependencies.map do |item|
23
+ item.split(' (')[0].split('/')[0]
24
+ end
25
+
26
+ # 从PODS依赖树构建"组件 -> 引用方"索引,用于输出来源
27
+ referrers = build_referrers(dependencies_lock['PODS'])
28
+
29
+ log = CocoaPods::Yaml.log
30
+ check_sums.each_key do |pod_name|
31
+ next if direct_dependencies.include?(pod_name)
32
+
33
+ parents = referrers[pod_name] || []
34
+ if parents.empty?
35
+ log.detail("间接依赖: #{pod_name}")
36
+ else
37
+ log.detail("间接依赖: #{pod_name}(由 #{parents.join('、')} 引入)")
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # 从Podfile.lock的PODS段解析引用关系
45
+ # @param pods [Array<String, Hash>] PODS段内容
46
+ # @return [Hash{String => Array<String>}] 组件名 -> 直接引用它的组件名(已去重)
47
+ def build_referrers(pods)
48
+ result = Hash.new { |hash, key| hash[key] = [] }
49
+ return result unless pods.is_a?(Array)
50
+
51
+ pods.each do |entry|
52
+ # PODS段中无下级依赖的组件是纯字符串,带依赖的是Hash
53
+ next unless entry.is_a?(Hash)
54
+
55
+ entry.each do |spec, child_dependencies|
56
+ parent = normalize_pod_name(spec)
57
+ Array(child_dependencies).each do |child|
58
+ child_name = normalize_pod_name(child)
59
+ result[child_name] << parent unless result[child_name].include?(parent)
60
+ end
61
+ end
62
+ end
63
+ result
64
+ end
65
+
66
+ # 归一化pod名: 去掉版本要求与subspec路径
67
+ # 例: "BSDebugPlugins/Cookie (= 1.2.0-SNAPSHOT)" -> "BSDebugPlugins"
68
+ def normalize_pod_name(name)
69
+ name.to_s.split(' (')[0].split('/')[0]
70
+ end
71
+ end
72
+ end