├── .github
└── workflows
│ └── rust.yml
├── .gitignore
├── .idea
├── .gitignore
├── RustPlayer.iml
├── modules.xml
└── vcs.xml
├── .vscode
└── launch.json
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── assets
└── test.lrc
├── build.rs
├── dynamic-lib.patch
├── radio.ini
├── setup.iss
├── snap
└── snapcraft.yaml
├── src
├── app.rs
├── config.rs
├── handler
│ ├── fs.rs
│ ├── help.rs
│ ├── mod.rs
│ ├── music_controller.rs
│ ├── player.rs
│ └── radio.rs
├── main.rs
├── media
│ ├── media.rs
│ ├── mod.rs
│ └── player.rs
├── ui
│ ├── effects.rs
│ ├── frame.rs
│ ├── fs.rs
│ ├── help.rs
│ ├── mod.rs
│ ├── music_board.rs
│ ├── play_list.rs
│ ├── progress.rs
│ └── radio.rs
└── util
│ ├── lyrics.rs
│ ├── m3u8.rs
│ ├── mod.rs
│ └── net.rs
├── tests
├── lyrics.rs
└── m3u8.rs
└── thirdparty
└── ffmpeg-decoder-rs
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── cli
├── Cargo.toml
└── src
│ └── main.rs
└── src
├── decoder.rs
├── error.rs
├── lib.rs
└── rodio.rs
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | RUST_TOOLCHAIN:
7 | description: RUST_TOOLCHAIN (stable or 1.73)
8 | default: "1.73"
9 | required: true
10 |
11 | schedule:
12 | - cron: "0 0 * * *"
13 | push:
14 | paths-ignore:
15 | - 'README.md'
16 | - '.github/**'
17 | branches: [ master ]
18 |
19 | pull_request:
20 | paths-ignore:
21 | - 'README.md'
22 | - '.github/**'
23 | branches: [ master ]
24 |
25 | env:
26 | CARGO_TERM_COLOR: always
27 | VCPKG_COMMIT_ID: 06c79a9afa6f99f02f44d20df9e0848b2a56bf1b
28 | TAG_NAME: latest
29 | VERSION: "1.1.2"
30 |
31 | jobs:
32 | build:
33 | runs-on: ${{ matrix.os }}
34 | continue-on-error: true
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | os: [ubuntu-20.04, macOS-latest]
39 | include:
40 | - name: linux
41 | os: ubuntu-20.04
42 | artifact_name: target/release/RustPlayer
43 | asset_name: RustPlayer-linux-amd64
44 | - name: macos
45 | os: macOS-latest
46 | artifact_name: target/release/RustPlayer
47 | asset_name: RustPlayer-macos
48 | steps:
49 | - uses: actions/checkout@v2
50 | - name: patch dynamic build
51 | run: git apply dynamic-lib.patch
52 |
53 | - run: rustup toolchain install ${{ inputs.RUST_TOOLCHAIN }} --profile minimal
54 | - uses: Swatinem/rust-cache@v2
55 |
56 | - name: install deps for linux
57 | if: matrix.os == 'ubuntu-20.04'
58 | run: sudo apt update && sudo apt install -y libasound2-dev libavcodec-dev libavformat-dev libswresample-dev libavutil-dev libavformat-dev pkg-config
59 |
60 | - name: install ffmpeg deps for macOS
61 | if: matrix.os == 'macOS-latest'
62 | run: brew install ffmpeg pkg-config && brew link ffmpeg
63 |
64 | - name: Install cargo bundle
65 | if: matrix.os == 'ubuntu-20.04'
66 | shell: bash
67 | run: |
68 | pushd /tmp
69 | git clone https://github.com/KetaDotCC/cargo-bundle
70 | pushd cargo-bundle
71 | cargo install --path .
72 | popd
73 | popd
74 |
75 | - name: build RustPlayer for macOS
76 | if: matrix.os == 'macOS-latest'
77 | run: cargo build --release
78 |
79 | - name: build RustPlayer for Linux
80 | if: matrix.os == 'ubuntu-20.04'
81 | run: cargo bundle --release
82 |
83 | - name: bundle RustPlayer for macOS
84 | if: matrix.os == 'macOS-latest'
85 | run: |
86 | # npm install --global create-dmg
87 | # for name in target/release/bundle/osx/*.app; do
88 | # create-dmg $name || true
89 | # done
90 | mv target/release/rustplayer rustplayer-binary-macos
91 |
92 | - name: Publish Release for linux
93 | if: matrix.os == 'ubuntu-20.04'
94 | uses: softprops/action-gh-release@v1
95 | with:
96 | prerelease: true
97 | tag_name: ${{ env.TAG_NAME }}
98 | files: |
99 | target/release/bundle/deb/*.deb
100 |
101 | - name: Publish Release for macos
102 | if: matrix.os == 'macOS-latest'
103 | uses: softprops/action-gh-release@v1
104 | with:
105 | prerelease: true
106 | tag_name: ${{ env.TAG_NAME }}
107 | files: |
108 | rustplayer-binary-macos
109 |
110 | build-on-windows:
111 | runs-on: windows-latest
112 | continue-on-error: true
113 | strategy:
114 | fail-fast: false
115 | matrix:
116 | include:
117 | - { sys: mingw64, env: x86_64, artifact_name: target/release/RustPlayer.exe , asset_name: RustPlayer-windows-x86_64.exe }
118 | # - { sys: mingw32, env: i686, artifact_name: target/release/RustPlayer.exe , asset_name: RustPlayer-windows-x86.exe }
119 | # - { sys: ucrt64, env: ucrt-x86_64 } # Experimental!
120 | # - { sys: clang64, env: clang-x86_64 } # Experimental!
121 | # defaults:
122 | # run:
123 | # shell: msys2 {0}
124 | steps:
125 | - uses: actions/checkout@v2
126 | - run: rustup toolchain install ${{ inputs.RUST_TOOLCHAIN }} --profile minimal
127 | - uses: Swatinem/rust-cache@v2
128 |
129 | - name: setup vcpkg (do not install any package)
130 | uses: lukka/run-vcpkg@v10
131 | with:
132 | vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
133 |
134 | - name: build RustPlayer
135 | shell: bash
136 | run: |
137 | $VCPKG_ROOT/vcpkg install ffmpeg:x64-windows-static-md
138 | cargo build --release
139 |
140 | - name: rename
141 | shell: bash
142 | run: |
143 | mv target/release/rustplayer.exe target/release/rustplayer-${{ env.VERSION }}.exe
144 |
145 | - name: Publish Release
146 | uses: softprops/action-gh-release@v1
147 | with:
148 | prerelease: true
149 | tag_name: ${{ env.TAG_NAME }}
150 | files: |
151 | target/release/rustplayer*.exe
152 |
153 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | *.mp3
3 | 音乐/**
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/RustPlayer.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // 使用 IntelliSense 了解相关属性。
3 | // 悬停以查看现有属性的描述。
4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "lldb",
9 | "request": "launch",
10 | "name": "Debug executable 'RustPlayer'",
11 | "cargo": {
12 | "args": [
13 | "build",
14 | "--bin=RustPlayer",
15 | "--package=RustPlayer"
16 | ],
17 | "filter": {
18 | "name": "RustPlayer",
19 | "kind": "bin"
20 | }
21 | },
22 | "args": [],
23 | "cwd": "${workspaceFolder}"
24 | },
25 | {
26 | "type": "lldb",
27 | "request": "launch",
28 | "name": "Debug unit tests in executable 'RustPlayer'",
29 | "cargo": {
30 | "args": [
31 | "test",
32 | "--no-run",
33 | "--bin=RustPlayer",
34 | "--package=RustPlayer"
35 | ],
36 | "filter": {
37 | "name": "RustPlayer",
38 | "kind": "bin"
39 | }
40 | },
41 | "args": [],
42 | "cwd": "${workspaceFolder}"
43 | }
44 | ]
45 | }
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rustplayer"
3 | version = "1.1.2"
4 | edition = "2021"
5 | description = "Music/Radio Player built by Rust"
6 | authors = ["KetaNetwork"]
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [dependencies]
11 | rand = "0.8.5"
12 |
13 | tui = "0.19.0"
14 | crossterm = "0.25.0"
15 |
16 | rodio = { version = "0.17", features = ["mp3", "wav", "flac"] }
17 | mp3-duration = "0.1.10"
18 |
19 | failure = "0.1.8"
20 |
21 | open = "2.1.0"
22 |
23 | regex = "1.5.4"
24 |
25 | reqwest = { version = "0.11", features = ["json"] }
26 | tokio = { version = "1", features = ["full"] }
27 |
28 | m3u8-rs = "3.0.0"
29 | dirs = "4.0.0"
30 |
31 | bytes = "1.1.0"
32 | ffmpeg-decoder = {features = ["rodio_source"], path = "thirdparty/ffmpeg-decoder-rs"}
33 | lazy_static = "1.4.0"
34 |
35 | [package.metadata.bundle]
36 | name = "RustPlayer"
37 | identifier = "cc.ketanetwork.rustplayer"
38 | # icon = ["32x32.png", "128x128.png", "128x128@2x.png"]
39 | version = "1.1.2"
40 | # resources = ["assets", "images/**/*.png", "secrets/public_key.txt"]
41 | copyright = "Copyright (c) KetaNetwork 2023. All rights reserved."
42 | category = "Music"
43 | short_description = "Music/Radio Player built by Rust."
44 | long_description = """
45 | Music/Radio Player built by Rust.
46 | """
47 | deb_depends = ["libasound2", "libavcodec57 | libavcodec58", "libavformat57 | libavformat58", "libswresample2 | libswresample3"]
48 | osx_minimum_system_version = "10.15"
49 |
50 | [profile]
51 | release = { strip = "symbols", lto = "thin", opt-level = "z" }
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RustPlayer [](https://GitHub.com/KetaNetwork/RustPlayer/tags/) [](https://github.com/KetaNetwork/RustPlayer/stargazers/)
2 |
3 | 
4 | [](https://snapcraft.io/rustplayer)
5 | [](https://snapcraft.io/rustplayer)
6 |
7 | [](https://svgshare.com/i/Zhy.svg)
8 | [](https://svgshare.com/i/ZjP.svg)
9 | [](https://svgshare.com/i/ZhY.svg)
10 | [](https://www.codacy.com/gh/KetaNetwork/RustPlayer/dashboard?utm_source=github.com&utm_medium=referral&utm_content=KetaNetwork/RustPlayer&utm_campaign=Badge_Grade)
11 | 
12 | []()
13 |
14 | An local audio player & network m3u8 radio player using completely terminal gui. MacOS, Linux, Windows are all supported.
15 |
16 | RustPlayer is under development. If u have encountered any problem, please open issues :)
17 |
18 | ## Features
19 |
20 | - Support mp3, wav, flac format
21 | - Support m3u8 network radio
22 | - tested: 央广之声、经济之声. check `radio.ini` for details.
23 | - please copy `radio.ini` to `~/.config/rustplayer`
24 | - Lyrics Supported
25 | - Multi-platform supported
26 | - Low CPU and memory usage
27 | - File explorer
28 | - Playlist playback supported
29 | - Wave animation
30 | - Playback progress
31 | - Next audio
32 | - Adjust volume
33 | - Developed by KetaNetwork
34 |
35 |
36 | ## Install RustPlayer by Snap Store
37 |
38 | `snap install rustplayer --devmode`
39 |
40 | ## Download Binary Release Directly and Run
41 |
42 | The binary release of macOS, Ubuntu/Debian Linux, Windows can be found in artifacts of [latest prerelease](https://github.com/KetaNetwork/RustPlayer/releases/tag/latest). Click the top item of the list to download the latest release.
43 |
44 | For Arch/Manjaro users, use `yay -S rustplayer` instead.
45 |
46 | ## Screenshots
47 |
48 | ### Windows
49 |
50 | Play with lyrics. If no lyrics found, the wave animation will be the replacement of the block. See screenshots from Linux and macOS below.
51 |
52 | 
53 |
54 | ### Linux
55 |
56 | The screenshot from Deepin
57 |
58 | 
59 |
60 | ### macOS
61 |
62 |
63 |
64 |
65 |
66 | ## Compile RustPlayer and run
67 |
68 | If u found this binary release is not working or u like compiling RustPlayer by youselef. Yes, The step to compile RustPlayer is really easy.
69 |
70 | - clone this repo.
71 | - for arch/manjaro, please use [fix/arch](https://github.com/KetaNetwork/RustPlayer/tree/fix/arch) branch.
72 | - install dependencies
73 | - check `.github/rust.yml` for details
74 | - `cargo run` in root of this project.
75 |
76 | if u think this repo is helpful, ⭐ this project and let me know :)
77 |
78 | ## TroubleShoot
79 |
80 | ### Linux
81 |
82 | Q: No sound in Linux, console shows "unable to open slave". I'm using `snd_hda_intel` drivers.
83 |
84 | A: check your valid sound card. Check by `lspci -knn|grep -iA2 audio`. An example is:
85 | ```
86 | 04:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Renoir Radeon High Definition Audio Controller [1002:1637]
87 | Subsystem: Lenovo Device [17aa:3814]
88 | Kernel driver in use: snd_hda_intel
89 | --
90 | 04:00.5 Multimedia controller [0480]: Advanced Micro Devices, Inc. [AMD] ACP/ACP3X/ACP6x Audio Coprocessor [1022:15e2] (rev 01)
91 | Subsystem: Lenovo Device [17aa:3832]
92 | Kernel modules: snd_pci_acp3x, snd_rn_pci_acp3x, snd_pci_acp5x
93 | 04:00.6 Audio device [0403]: Advanced Micro Devices, Inc. [AMD] Family 17h/19h HD Audio Controller [1022:15e3]
94 | Subsystem: Lenovo Device [17aa:3833]
95 | Kernel driver in use: snd_hda_intel
96 | ```
97 |
98 | In the case above, 2 audio devices found in your Linux. Let's check which device is in use, we will use `index` to identify the default device. Type `modinfo snd_hda_intel | grep index`, if only shows:
99 |
100 | ```
101 | parm: index:Index value for Intel HD audio interface. (array of int)
102 | ```
103 |
104 | which means index 0 will be chosen to be the default output device.
105 |
106 | In this case, you can try device 1. create files below:
107 | ```shell
108 | > cat /etc/modprobe.d/default.conf
109 |
110 | options snd_hda_intel index=1
111 | ```
112 |
113 | reboot and check if it works.
114 |
--------------------------------------------------------------------------------
/assets/test.lrc:
--------------------------------------------------------------------------------
1 | [00:00.000] 作曲 : Peter-潘
2 | [00:00.577] 作词 : 宋普照
3 | [00:01.733]编曲Arranger:刘也
4 | [00:05.786]
5 | [00:07.426]我心跳的小马达 哒哒哒哒哒哒哒
6 | [00:11.467]加速着荷尔蒙 红了你脸颊
7 | [00:15.933]想送你回家 在你家楼下
8 | [00:20.118]燃放心形的烟花
9 | [00:23.612]
10 | [00:24.064]灰色的马甲
11 | [00:27.618]遮阳帽下你通着电话
12 | [00:32.892]飘在行人道前面的晚霞
13 | [00:36.878]好想采下几朵为你变魔法
14 | [00:40.533]
15 | [00:41.901]路边的鲜花
16 | [00:46.327]冰淇淋在你唇角融化
17 | [00:49.816]跟我一起来喝一杯好吗
18 | [00:53.810]就当消磨时间顺便恋爱吧
19 | [00:58.569]
20 | [00:59.784]我心跳的小马达 哒哒哒哒哒哒哒
21 | [01:04.214]加速着荷尔蒙 红了你脸颊
22 | [01:08.649]想送你回家 在你家楼下
23 | [01:12.853]燃放心形的烟花
24 | [01:16.479]
25 | [01:16.753]我心跳的小马达 哒哒哒哒哒哒哒
26 | [01:21.171]故事的剧情在 推进着情话
27 | [01:25.533]心门的密码 解锁了几下
28 | [01:29.736]请将我的爱收下
29 | [01:33.903]
30 | [01:50.945]灰色的马甲
31 | [01:56.194]遮阳帽下你通着电话
32 | [01:59.794]飘在行人道前面的晚霞
33 | [02:03.930]好想采下几朵为你变魔法
34 | [02:07.829]
35 | [02:09.000]路边的鲜花
36 | [02:13.296]冰淇淋在你唇角融化
37 | [02:16.629]跟我一起来喝一杯好吗
38 | [02:20.427]就当消磨时间顺便恋爱吧
39 | [02:24.386]
40 | [02:24.604]我心跳的小马达 哒哒哒哒哒哒哒
41 | [02:28.815]加速着荷尔蒙 红了你脸颊
42 | [02:33.251]想送你回家 在你家楼下
43 | [02:37.503]燃放心形的烟花
44 | [02:41.101]
45 | [02:41.318]我心跳的小马达 哒哒哒哒哒哒哒
46 | [02:45.699]故事的剧情在 推进着情话
47 | [02:50.136]心门的密码 解锁了几下
48 | [02:54.283]请将我的爱收下
49 | [02:58.466]
50 | [02:58.670]我心跳的小马达 哒哒哒哒哒哒哒
51 | [03:02.648]加速着荷尔蒙 红了你脸颊
52 | [03:07.076]想送你回家 在你家楼下
53 | [03:11.299]燃放心形的烟花
54 | [03:15.200]
55 | [03:15.370]我心跳的小马达 哒哒哒哒哒哒哒
56 | [03:19.468]故事的剧情在 推进着情话
57 | [03:23.953]心门的密码 解锁了几下
58 | [03:28.212]请将我的爱收下
59 | [03:32.451]
60 | [03:33.043]制作人Producer:顾雄/胡小健
61 | [03:34.053]吉他Guitarist:顾雄
62 | [03:35.051]和声编写Harmony Arrangement:曾婕
63 | [03:36.050]和声Harmony Vocals:曾婕
64 | [03:37.096]人声录音工程师Recording Engineer:李杨/刘芒@55TEC
65 | [03:38.086]人声录音室Vocal Recording Studio:55TEC
66 | [03:39.085]混音工程师Mixing Engineer:顾雄
67 | [03:40.043]母带工程师Mastering:顾雄
68 | [03:41.042]封面设计Cover Design:高长长
69 | [03:42.016]出品人Publisher:胡小健
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | #[cfg(target_os = "macos")]
2 | fn emit_minimum_version() {
3 | println!("set minimum version to 10.15");
4 | println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15");
5 | }
6 |
7 | fn configure() {
8 | #[cfg(target_os = "macos")]
9 | {
10 | emit_minimum_version();
11 | }
12 | }
13 |
14 | fn main() {
15 | println!("hello from RustPlayer!");
16 | configure();
17 | }
18 |
--------------------------------------------------------------------------------
/dynamic-lib.patch:
--------------------------------------------------------------------------------
1 | diff --git a/thirdparty/ffmpeg-decoder-rs/Cargo.toml b/thirdparty/ffmpeg-decoder-rs/Cargo.toml
2 | index 9db9ac9..4546253 100644
3 | --- a/thirdparty/ffmpeg-decoder-rs/Cargo.toml
4 | +++ b/thirdparty/ffmpeg-decoder-rs/Cargo.toml
5 | @@ -20,7 +20,7 @@ default = []
6 | rodio_source = ['rodio']
7 |
8 | [dependencies]
9 | -ffmpeg-sys-next = { git="https://github.com/KetaDotCC/rust-ffmpeg-sys.git", branch="master", default-features=false, features=["avcodec", "avformat", "swresample", "static"] }
10 | +ffmpeg-sys-next = { git="https://github.com/KetaDotCC/rust-ffmpeg-sys.git", branch="master", default-features=false, features=["avcodec", "avformat", "swresample"] }
11 |
12 | thiserror = "1.0"
13 | log = "0.4"
14 |
--------------------------------------------------------------------------------
/radio.ini:
--------------------------------------------------------------------------------
1 | 经济之声 http://ngcdn002.cnr.cn/live/jjzs/index.m3u8
2 | 中国之声 http://ngcdn001.cnr.cn/live/zgzs/index.m3u8
--------------------------------------------------------------------------------
/setup.iss:
--------------------------------------------------------------------------------
1 | ; Script generated by the Inno Setup Script Wizard.
2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 |
4 | #define MyAppName "RustPlayer"
5 | #define MyAppVersion "1.1.0-2"
6 | #define MyAppPublisher "KetaNetwork"
7 | #define MyAppURL "https:/KetaNetwork.cn"
8 | #define MyAppExeName "RustPlayer.exe"
9 | #define MSYS2 "D:\msys64\mingw64"
10 |
11 | [Setup]
12 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
14 | AppId={{DF52D5F1-364E-4810-82C6-1D5BAC310B85}
15 | AppName={#MyAppName}
16 | AppVersion={#MyAppVersion}
17 | ;AppVerName={#MyAppName} {#MyAppVersion}
18 | AppPublisher={#MyAppPublisher}
19 | AppPublisherURL={#MyAppURL}
20 | AppSupportURL={#MyAppURL}
21 | AppUpdatesURL={#MyAppURL}
22 | DefaultDirName={autopf}\{#MyAppName}
23 | DisableProgramGroupPage=yes
24 | ; Uncomment the following line to run in non administrative install mode (install for current user only.)
25 | ;PrivilegesRequired=lowest
26 | OutputBaseFilename=rustplayer-setup
27 | Compression=lzma
28 | SolidCompression=yes
29 | WizardStyle=modern
30 |
31 | [Languages]
32 | Name: "english"; MessagesFile: "compiler:Default.isl"
33 |
34 | [Tasks]
35 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
36 |
37 | [Files]
38 | Source: "target\release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
39 | ; deps
40 | Source: "{#MSYS2}\bin\avformat-58.dll"; DestDir: "{app}"; Flags: ignoreversion
41 | Source: "{#MSYS2}\bin\avcodec-58.dll"; DestDir: "{app}"; Flags: ignoreversion
42 | Source: "{#MSYS2}\bin\avutil-56.dll"; DestDir: "{app}"; Flags: ignoreversion
43 | Source: "{#MSYS2}\bin\swresample-3.dll"; DestDir: "{app}"; Flags: ignoreversion
44 | Source: "{#MSYS2}\bin\libbluray-2.dll"; DestDir: "{app}"; Flags: ignoreversion
45 | Source: "{#MSYS2}\bin\libbz2-1.dll"; DestDir: "{app}"; Flags: ignoreversion
46 | Source: "{#MSYS2}\bin\libgme.dll"; DestDir: "{app}"; Flags: ignoreversion
47 | Source: "{#MSYS2}\bin\libiconv-2.dll"; DestDir: "{app}"; Flags: ignoreversion
48 | Source: "{#MSYS2}\bin\libgnutls-30.dll"; DestDir: "{app}"; Flags: ignoreversion
49 | Source: "{#MSYS2}\bin\libmodplug-1.dll"; DestDir: "{app}"; Flags: ignoreversion
50 | Source: "{#MSYS2}\bin\librtmp-1.dll"; DestDir: "{app}"; Flags: ignoreversion
51 | Source: "{#MSYS2}\bin\libsrt.dll"; DestDir: "{app}"; Flags: ignoreversion
52 | Source: "{#MSYS2}\bin\libcairo-2.dll"; DestDir: "{app}"; Flags: ignoreversion
53 | Source: "{#MSYS2}\bin\libaom.dll"; DestDir: "{app}"; Flags: ignoreversion
54 | Source: "{#MSYS2}\bin\libssh.dll"; DestDir: "{app}"; Flags: ignoreversion
55 | Source: "{#MSYS2}\bin\libcelt0-2.dll"; DestDir: "{app}"; Flags: ignoreversion
56 | Source: "{#MSYS2}\bin\libdav1d.dll"; DestDir: "{app}"; Flags: ignoreversion
57 | Source: "{#MSYS2}\bin\libxml2-2.dll"; DestDir: "{app}"; Flags: ignoreversion
58 | Source: "{#MSYS2}\bin\libglib-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
59 | Source: "{#MSYS2}\bin\libgobject-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
60 | Source: "{#MSYS2}\bin\zlib1.dll"; DestDir: "{app}"; Flags: ignoreversion
61 | Source: "{#MSYS2}\bin\libgsm.dll"; DestDir: "{app}"; Flags: ignoreversion
62 | Source: "{#MSYS2}\bin\libintl-8.dll"; DestDir: "{app}"; Flags: ignoreversion
63 | Source: "{#MSYS2}\bin\liblzma-5.dll"; DestDir: "{app}"; Flags: ignoreversion
64 | Source: "{#MSYS2}\bin\libmfx-1.dll"; DestDir: "{app}"; Flags: ignoreversion
65 | Source: "{#MSYS2}\bin\libmp3lame-0.dll"; DestDir: "{app}"; Flags: ignoreversion
66 | Source: "{#MSYS2}\bin\libopencore-amrnb-0.dll"; DestDir: "{app}"; Flags: ignoreversion
67 | Source: "{#MSYS2}\bin\libopencore-amrwb-0.dll"; DestDir: "{app}"; Flags: ignoreversion
68 | Source: "{#MSYS2}\bin\libwinpthread-1.dll"; DestDir: "{app}"; Flags: ignoreversion
69 | Source: "{#MSYS2}\bin\libopenjp2-7.dll"; DestDir: "{app}"; Flags: ignoreversion
70 | Source: "{#MSYS2}\bin\libvulkan-1.dll"; DestDir: "{app}"; Flags: ignoreversion
71 | Source: "{#MSYS2}\bin\libopus-0.dll"; DestDir: "{app}"; Flags: ignoreversion
72 | Source: "{#MSYS2}\bin\libspeex-1.dll"; DestDir: "{app}"; Flags: ignoreversion
73 | Source: "{#MSYS2}\bin\librsvg-2-2.dll"; DestDir: "{app}"; Flags: ignoreversion
74 | Source: "{#MSYS2}\bin\rav1e.dll"; DestDir: "{app}"; Flags: ignoreversion
75 | Source: "{#MSYS2}\bin\libSvtAv1Enc.dll"; DestDir: "{app}"; Flags: ignoreversion
76 | Source: "{#MSYS2}\bin\libtheoradec-1.dll"; DestDir: "{app}"; Flags: ignoreversion
77 | Source: "{#MSYS2}\bin\libtheoraenc-1.dll"; DestDir: "{app}"; Flags: ignoreversion
78 | Source: "{#MSYS2}\bin\libvorbis-0.dll"; DestDir: "{app}"; Flags: ignoreversion
79 | Source: "{#MSYS2}\bin\libvorbisenc-2.dll"; DestDir: "{app}"; Flags: ignoreversion
80 | Source: "{#MSYS2}\bin\libvpx-1.dll"; DestDir: "{app}"; Flags: ignoreversion
81 | Source: "{#MSYS2}\bin\libwebp-7.dll"; DestDir: "{app}"; Flags: ignoreversion
82 | Source: "{#MSYS2}\bin\libwebpmux-3.dll"; DestDir: "{app}"; Flags: ignoreversion
83 | Source: "{#MSYS2}\bin\libx264-164.dll"; DestDir: "{app}"; Flags: ignoreversion
84 | Source: "{#MSYS2}\bin\libx265.dll"; DestDir: "{app}"; Flags: ignoreversion
85 | Source: "{#MSYS2}\bin\xvidcore.dll"; DestDir: "{app}"; Flags: ignoreversion
86 | Source: "{#MSYS2}\bin\libsoxr.dll"; DestDir: "{app}"; Flags: ignoreversion
87 | Source: "{#MSYS2}\bin\libfreetype-6.dll"; DestDir: "{app}"; Flags: ignoreversion
88 | Source: "{#MSYS2}\bin\libgcc_s_seh-1.dll"; DestDir: "{app}"; Flags: ignoreversion
89 | Source: "{#MSYS2}\bin\libstdc++-6.dll"; DestDir: "{app}"; Flags: ignoreversion
90 | Source: "{#MSYS2}\bin\libgmp-10.dll"; DestDir: "{app}"; Flags: ignoreversion
91 | Source: "{#MSYS2}\bin\libhogweed-6.dll"; DestDir: "{app}"; Flags: ignoreversion
92 | Source: "{#MSYS2}\bin\libidn2-0.dll"; DestDir: "{app}"; Flags: ignoreversion
93 | Source: "{#MSYS2}\bin\libnettle-8.dll"; DestDir: "{app}"; Flags: ignoreversion
94 | Source: "{#MSYS2}\bin\libp11-kit-0.dll"; DestDir: "{app}"; Flags: ignoreversion
95 | Source: "{#MSYS2}\bin\libtasn1-6.dll"; DestDir: "{app}"; Flags: ignoreversion
96 | Source: "{#MSYS2}\bin\libunistring-2.dll"; DestDir: "{app}"; Flags: ignoreversion
97 | Source: "{#MSYS2}\bin\libcrypto-1_1-x64.dll"; DestDir: "{app}"; Flags: ignoreversion
98 | Source: "{#MSYS2}\bin\libfontconfig-1.dll"; DestDir: "{app}"; Flags: ignoreversion
99 | Source: "{#MSYS2}\bin\libpixman-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion
100 | Source: "{#MSYS2}\bin\libpng16-16.dll"; DestDir: "{app}"; Flags: ignoreversion
101 | Source: "{#MSYS2}\bin\libffi-7.dll"; DestDir: "{app}"; Flags: ignoreversion
102 | Source: "{#MSYS2}\bin\libpcre-1.dll"; DestDir: "{app}"; Flags: ignoreversion
103 | Source: "{#MSYS2}\bin\libssp-0.dll"; DestDir: "{app}"; Flags: ignoreversion
104 | Source: "{#MSYS2}\bin\libcairo-gobject-2.dll"; DestDir: "{app}"; Flags: ignoreversion
105 | Source: "{#MSYS2}\bin\libgdk_pixbuf-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
106 | Source: "{#MSYS2}\bin\libpango-1.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
107 | Source: "{#MSYS2}\bin\libgio-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
108 | Source: "{#MSYS2}\bin\libpangocairo-1.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
109 | Source: "{#MSYS2}\bin\libogg-0.dll"; DestDir: "{app}"; Flags: ignoreversion
110 | Source: "{#MSYS2}\bin\libgomp-1.dll"; DestDir: "{app}"; Flags: ignoreversion
111 | Source: "{#MSYS2}\bin\libbrotlidec.dll"; DestDir: "{app}"; Flags: ignoreversion
112 | Source: "{#MSYS2}\bin\libharfbuzz-0.dll"; DestDir: "{app}"; Flags: ignoreversion
113 | Source: "{#MSYS2}\bin\libexpat-1.dll"; DestDir: "{app}"; Flags: ignoreversion
114 | Source: "{#MSYS2}\bin\libgmodule-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
115 | Source: "{#MSYS2}\bin\libfribidi-0.dll"; DestDir: "{app}"; Flags: ignoreversion
116 | Source: "{#MSYS2}\bin\libthai-0.dll"; DestDir: "{app}"; Flags: ignoreversion
117 | Source: "{#MSYS2}\bin\libpangoft2-1.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
118 | Source: "{#MSYS2}\bin\libpangowin32-1.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion
119 | Source: "{#MSYS2}\bin\libbrotlicommon.dll"; DestDir: "{app}"; Flags: ignoreversion
120 | Source: "{#MSYS2}\bin\libgraphite2.dll"; DestDir: "{app}"; Flags: ignoreversion
121 | Source: "{#MSYS2}\bin\libdatrie-1.dll"; DestDir: "{app}"; Flags: ignoreversion
122 |
123 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
124 |
125 | [Icons]
126 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
127 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
128 |
129 | [Run]
130 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
131 |
132 |
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: rustplayer
2 | version: '1.1.0-2'
3 | summary: A local music player && network radio player
4 | description: |
5 | RustPlayer is a local music player && network radio player:
6 |
7 | base: core18
8 | confinement: strict
9 |
10 | architectures:
11 | - build-on: i386
12 | - build-on: amd64
13 | - build-on: arm64
14 | - build-on: armhf
15 | - build-on: ppc64el
16 |
17 | parts:
18 | rustplayer:
19 | plugin: rust
20 | source: .
21 | override-pull: |
22 | snapcraftctl pull
23 | git apply dynamic-lib.patch
24 | build-packages:
25 | - libasound2-dev
26 | - libavcodec-dev
27 | - libavformat-dev
28 | - libswresample-dev
29 | - libavutil-dev
30 | - pkg-config
31 | - libssl-dev
32 | - clang
33 | - git
34 | stage-packages:
35 | - libasound2
36 | - libavcodec57
37 | - libavformat57
38 | - libswresample2
39 | - libavutil55
40 |
41 | layout:
42 | /usr/share/alsa:
43 | bind: $SNAP/usr/share/alsa
44 |
45 | apps:
46 | rustplayer:
47 | command: bin/rustplayer
48 | plugs:
49 | - network
50 | - audio-playback
51 | - home
52 | # connect manually
53 | - pulseaudio
54 | - alsa
55 | - removable-media
56 |
--------------------------------------------------------------------------------
/src/app.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::{
19 | io::stdout,
20 | sync::mpsc,
21 | thread::{self},
22 | vec,
23 | };
24 |
25 | use crossterm::{
26 | event::{self, Event, KeyCode},
27 | execute,
28 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
29 | };
30 | use failure::Error;
31 | use tui::{
32 | backend::{Backend, CrosstermBackend},
33 | layout::{Alignment, Constraint, Direction, Layout, Rect},
34 | style::{Color, Style},
35 | text::Text,
36 | widgets::{Block, BorderType, Borders, ListState, Paragraph, Wrap},
37 | Frame, Terminal,
38 | };
39 |
40 | use crate::{
41 | config::Config,
42 | fs::FsExplorer,
43 | handler::handle_keyboard_event,
44 | media::player::{MusicPlayer, Player, RadioPlayer},
45 | ui::{
46 | fs::draw_fs_tree,
47 | help::draw_help,
48 | music_board::{draw_music_board, MusicController},
49 | radio::{draw_radio_list, RadioExplorer},
50 | EventType,
51 | },
52 | util::m3u8::empty_cache,
53 | };
54 |
55 | pub enum InputMode {
56 | Normal,
57 | }
58 |
59 | #[derive(Clone, Copy, PartialEq)]
60 | pub enum Routes {
61 | Main,
62 | Help,
63 | }
64 |
65 | #[derive(PartialEq)]
66 | pub enum ActiveModules {
67 | Fs,
68 | RadioList,
69 | }
70 |
71 | pub struct App {
72 | pub mode: InputMode,
73 | pub fs: FsExplorer,
74 | pub radio_fs: RadioExplorer,
75 | pub route_stack: Vec,
76 | pub player: MusicPlayer,
77 | pub radio: RadioPlayer,
78 | pub music_controller: MusicController,
79 | pub active_modules: ActiveModules,
80 | pub config: Config,
81 | // terminal: Option>,
82 | msg: String,
83 | }
84 |
85 | impl App {
86 | pub fn new() -> Option {
87 | Some(Self {
88 | mode: InputMode::Normal,
89 | fs: FsExplorer::default(Some(|err| {
90 | eprintln!("{}", err);
91 | }))
92 | .ok()?,
93 | // terminal: None,
94 | route_stack: vec![Routes::Main],
95 | player: Player::new(),
96 | radio: Player::new(),
97 | radio_fs: RadioExplorer::new(),
98 | music_controller: MusicController {
99 | state: ListState::default(),
100 | },
101 | active_modules: ActiveModules::Fs,
102 | msg: "Welcome to RustPlayer".to_string(),
103 | config: Config::default(),
104 | })
105 | }
106 |
107 | // block thread and show screen
108 | pub fn run(&mut self) -> Result<(), Error> {
109 | let mut stdout = stdout();
110 | execute!(stdout, EnterAlternateScreen)?;
111 | let backend = CrosstermBackend::new(stdout);
112 | let mut terminal = Terminal::new(backend)?;
113 | // execute!(terminal.backend_mut(), EnableMouseCapture);
114 | enable_raw_mode()?;
115 | terminal.hide_cursor()?;
116 | self.draw_frame(&mut terminal)?;
117 | // tick daemon thread
118 | let (sd, rd) = mpsc::channel::();
119 | let tick = self.config.tick_gap.clone();
120 | thread::spawn(move || loop {
121 | thread::sleep(tick);
122 | let _ = sd.send(EventType::Player);
123 | let _ = sd.send(EventType::Radio);
124 | });
125 | // start event
126 | let (evt_sender, evt_receiver) = mpsc::sync_channel(1);
127 | let (exit_sender, exit_receiver) = mpsc::channel();
128 | let evt_th = thread::spawn(move || loop {
129 | let evt = event::read();
130 | match evt {
131 | Ok(evt) => {
132 | if let Event::Key(key) = evt {
133 | match self.mode {
134 | InputMode::Normal => match key.code {
135 | KeyCode::Char('q') | KeyCode::Char('Q') => {
136 | empty_cache();
137 | drop(evt_sender);
138 | let _ = exit_sender.send(());
139 | return;
140 | }
141 | code => {
142 | match evt_sender.send(code) {
143 | Ok(_) => {}
144 | Err(_) => {
145 | // send error, exit.
146 | return;
147 | }
148 | }
149 | }
150 | },
151 | }
152 | }
153 | }
154 | Err(_) => {
155 | // exit.
156 | return;
157 | }
158 | }
159 | });
160 | loop {
161 | thread::sleep(self.config.refresh_rate);
162 | if let Ok(_) = exit_receiver.try_recv() {
163 | break;
164 | }
165 | match evt_receiver.try_recv() {
166 | Ok(code) => handle_keyboard_event(self, code),
167 | _ => {}
168 | }
169 | // 10 fps
170 | self.draw_frame(&mut terminal)?;
171 | if let Ok(event) = rd.try_recv() {
172 | self.handle_events(event);
173 | }
174 | }
175 | disable_raw_mode()?;
176 | execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
177 | terminal.show_cursor()?;
178 | let _ = evt_th.join();
179 | Ok(())
180 | }
181 |
182 | fn handle_events(&mut self, event: EventType) {
183 | // event
184 | match event {
185 | EventType::Player => {
186 | let player = &mut self.player;
187 | player.tick();
188 | }
189 | EventType::Radio => {
190 | let radio = &mut self.radio;
191 | radio.tick();
192 | }
193 | }
194 | }
195 |
196 | pub fn draw_frame(&mut self, terminal: &mut Terminal) -> Result<(), Error>
197 | where
198 | B: Backend,
199 | {
200 | terminal.draw(|frame| {
201 | let size = frame.size();
202 | let chunks = Layout::default()
203 | .direction(Direction::Vertical)
204 | .margin(4)
205 | .constraints([Constraint::Length(3), Constraint::Percentage(100)].as_ref())
206 | .split(size);
207 | if self.route_stack.is_empty() {
208 | self.route_stack.push(Routes::Main);
209 | }
210 | let route = self.route_stack.last().unwrap();
211 | match route {
212 | Routes::Main => {
213 | self.draw_header(frame, chunks[0]);
214 | self.draw_body(frame, chunks[1]).unwrap();
215 | }
216 | Routes::Help => {
217 | self.draw_header(frame, chunks[0]);
218 | draw_help(self, frame, chunks[1]);
219 | }
220 | }
221 | })?;
222 | Ok(())
223 | }
224 |
225 | pub fn draw_header(&mut self, frame: &mut Frame, area: Rect)
226 | where
227 | B: Backend,
228 | {
229 | let block = Block::default()
230 | .title("RustPlayer - Music Player For Rust")
231 | .borders(Borders::ALL)
232 | .title_alignment(Alignment::Left)
233 | .border_type(BorderType::Rounded)
234 | .style(Style::default().fg(Color::White));
235 | let msg_p = Paragraph::new(Text::from(self.msg.as_str()))
236 | .style(Style::default().fg(Color::White))
237 | .alignment(Alignment::Center)
238 | .block(block)
239 | .wrap(Wrap { trim: true });
240 | // total
241 | frame.render_widget(msg_p, area);
242 | }
243 |
244 | pub fn draw_body(&mut self, frame: &mut Frame, area: Rect) -> Result<(), Error>
245 | where
246 | B: Backend,
247 | {
248 | let route = self.route_stack.first().unwrap();
249 | match route {
250 | Routes::Main => {
251 | let main_layout = Layout::default()
252 | .direction(Direction::Horizontal)
253 | .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
254 | // .margin(2)
255 | .split(area);
256 | // 左侧
257 | if self.active_modules == ActiveModules::RadioList {
258 | draw_radio_list(self, frame, main_layout[0]);
259 | } else {
260 | draw_fs_tree(self, frame, main_layout[0]);
261 | }
262 | // 右侧
263 | draw_music_board(self, frame, main_layout[1]);
264 | }
265 | Routes::Help => {
266 | draw_help(self, frame, area);
267 | }
268 | }
269 | Ok(())
270 | }
271 |
272 | pub fn set_msg(&mut self, msg: &str) {
273 | self.msg = String::from(msg);
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::time::Duration;
19 |
20 | pub struct Config {
21 | pub refresh_rate: Duration,
22 | pub tick_gap: Duration,
23 | pub home_page: &'static str,
24 | }
25 |
26 | impl Config {
27 | pub fn default() -> Self {
28 | Self {
29 | refresh_rate: Duration::from_millis(15),
30 | tick_gap: Duration::from_millis(100),
31 | home_page: "https://github.com/KetaNetwork",
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/handler/fs.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::{
19 | cmp::{max, min},
20 | env::{current_dir, set_current_dir},
21 | };
22 |
23 | use crossterm::event::KeyCode;
24 |
25 | use crate::{
26 | app::{ActiveModules, App, Routes},
27 | media::{
28 | media::{Media, Source},
29 | player::Player,
30 | },
31 | };
32 |
33 | fn add_media_to_player(app: &mut App, once: bool) -> bool {
34 | let fse = &mut app.fs;
35 | if let Some(selected) = fse.index.selected() {
36 | if selected <= fse.dirs.len() {
37 | let dir = current_dir().unwrap();
38 | // 返回上一级[0],文件夹
39 | match selected {
40 | 0 => match dir.parent() {
41 | Some(dir) => {
42 | set_current_dir(dir).unwrap();
43 | fse.current_path = dir.to_string_lossy().to_string();
44 | fse.index.select(Some(0));
45 | }
46 | None => {
47 | return false;
48 | }
49 | },
50 | num => {
51 | let dir_entry = &fse.dirs[num - 1];
52 | let path = dir_entry.path();
53 | fse.current_path = String::from(path.to_string_lossy());
54 | set_current_dir(path).unwrap();
55 | fse.index.select(Some(0));
56 | }
57 | }
58 | fse.refresh();
59 | return true;
60 | } else {
61 | // 文件
62 | let entry = &fse.files[selected - fse.dirs.len() - 1];
63 | let mut res = app.player.add_to_list(
64 | Media {
65 | src: Source::Local(entry.file_name().to_string_lossy().to_string()),
66 | },
67 | once,
68 | );
69 | // opt: add all music below
70 | if once {
71 | for i in selected - fse.dirs.len()..fse.files.len() {
72 | let entry = &fse.files[i];
73 | res = app.player.add_to_list(
74 | Media {
75 | src: Source::Local(entry.file_name().to_string_lossy().to_string()),
76 | },
77 | false,
78 | );
79 | }
80 | }
81 | if !res {
82 | let msg = format!("Open failed: {}", entry.file_name().to_str().unwrap());
83 | app.set_msg(&msg);
84 | } else {
85 | let msg = format!("Start playing");
86 | app.set_msg(&msg);
87 | }
88 | return res;
89 | }
90 | } else {
91 | fse.index.select(Some(0));
92 | return false;
93 | }
94 | }
95 |
96 | pub fn handle_fs(app: &mut App, key: KeyCode) -> bool {
97 | if app.active_modules != ActiveModules::Fs {
98 | return false;
99 | }
100 | match app.route_stack.first() {
101 | Some(route) => {
102 | if *route != Routes::Main {
103 | return false;
104 | }
105 | }
106 | None => {
107 | return false;
108 | }
109 | }
110 | let fse = &mut app.fs;
111 | let len = fse.dirs.len() + fse.files.len();
112 | match key {
113 | KeyCode::Down => {
114 | if let Some(selected) = fse.index.selected() {
115 | if selected == len {
116 | fse.index.select(Some(0));
117 | } else {
118 | fse.index.select(Some(min(len, selected + 1)));
119 | }
120 | return true;
121 | } else {
122 | fse.index.select(Some(0));
123 | }
124 | }
125 | KeyCode::Up => {
126 | if let Some(selected) = fse.index.selected() {
127 | if selected == 0 {
128 | fse.index.select(Some(len));
129 | return true;
130 | }
131 | fse.index.select(Some(max(0, selected - 1)));
132 | return true;
133 | } else {
134 | fse.index.select(Some(0));
135 | }
136 | }
137 | KeyCode::Right => {
138 | add_media_to_player(app, false);
139 | }
140 | KeyCode::Enter => {
141 | add_media_to_player(app, true);
142 | }
143 | _ => {}
144 | }
145 | false
146 | }
147 |
--------------------------------------------------------------------------------
/src/handler/help.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use crossterm::event::KeyCode;
19 |
20 | use crate::app::App;
21 |
22 | pub fn handle_help(app: &mut App, code: KeyCode) -> bool {
23 | match code {
24 | KeyCode::Enter => {
25 | open::that(app.config.home_page).unwrap();
26 | return true;
27 | }
28 | KeyCode::Char('r') => {
29 | let mut config_dir = dirs::config_dir().unwrap();
30 | config_dir.push("RustPlayer");
31 | open::that(config_dir).unwrap();
32 | return true;
33 | }
34 | _ => {
35 | return false;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/handler/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use crossterm::event::KeyCode;
19 |
20 | use crate::app::{ActiveModules, App, Routes};
21 |
22 | use self::{
23 | fs::handle_fs,
24 | help::handle_help,
25 | music_controller::{handle_music_controller, handle_radio_controller},
26 | player::{handle_player, handle_radio},
27 | radio::handle_radio_fs,
28 | };
29 |
30 | mod fs;
31 | mod help;
32 | mod music_controller;
33 | mod player;
34 | mod radio;
35 |
36 | pub fn handle_active_modules(app: &mut App, key: KeyCode) -> bool {
37 | match key {
38 | KeyCode::Tab => {
39 | if app.active_modules == ActiveModules::Fs {
40 | app.active_modules = ActiveModules::RadioList;
41 | } else if app.active_modules == ActiveModules::RadioList {
42 | app.active_modules = ActiveModules::Fs;
43 | }
44 | return true;
45 | }
46 | _ => {}
47 | }
48 | false
49 | }
50 |
51 | pub fn handle_routes(app: &mut App, key: KeyCode) -> bool {
52 | match key {
53 | KeyCode::Char('h') | KeyCode::Char('H') => {
54 | if let Some(page) = app.route_stack.last() {
55 | match page {
56 | Routes::Main => {
57 | app.route_stack.push(Routes::Help);
58 | }
59 | Routes::Help => {
60 | app.route_stack.pop();
61 | }
62 | }
63 | }
64 | return true;
65 | }
66 | _ => {}
67 | }
68 | false
69 | }
70 |
71 | pub fn handle_keyboard_event(app: &mut App, key: KeyCode) {
72 | let mut flag;
73 | let top_route = app.route_stack.last().unwrap();
74 |
75 | match top_route {
76 | Routes::Main => {
77 | flag = handle_active_modules(app, key);
78 | if flag {
79 | return;
80 | }
81 | match app.active_modules {
82 | ActiveModules::Fs => {
83 | flag = handle_fs(app, key);
84 | if flag {
85 | return;
86 | }
87 | flag = handle_player(app, key);
88 | if flag {
89 | return;
90 | }
91 | flag = handle_music_controller(app, key);
92 | if flag {
93 | return;
94 | }
95 | }
96 | ActiveModules::RadioList => {
97 | flag = handle_radio_fs(app, key);
98 | if flag {
99 | return;
100 | }
101 | flag = handle_radio(app, key);
102 | if flag {
103 | return;
104 | }
105 | flag = handle_radio_controller(app, key);
106 | if flag {
107 | return;
108 | }
109 | }
110 | }
111 | }
112 | Routes::Help => {
113 | flag = handle_help(app, key);
114 | if flag {
115 | return;
116 | }
117 | }
118 | }
119 | flag = handle_routes(app, key);
120 | if flag {
121 | return;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/handler/music_controller.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use crossterm::event::KeyCode;
19 |
20 | use crate::{app::App, media::player::Player};
21 |
22 | pub fn handle_music_controller(app: &mut App, code: KeyCode) -> bool {
23 | // if app.active_modules != ActiveModules::MusicController {
24 | // return false;
25 | // }
26 | let player = &mut app.player;
27 | match code {
28 | KeyCode::Char('s') | KeyCode::Char('S') => {
29 | if player.is_playing() {
30 | player.pause();
31 | } else {
32 | player.resume();
33 | }
34 | return true;
35 | }
36 | KeyCode::Char('n') | KeyCode::Char('N') => {
37 | player.next();
38 | return true;
39 | }
40 | _ => {
41 | return false;
42 | }
43 | }
44 | }
45 |
46 | pub fn handle_radio_controller(app: &mut App, code: KeyCode) -> bool {
47 | // if app.active_modules != ActiveModules::MusicController {
48 | // return false;
49 | // }
50 | let player = &mut app.radio;
51 | match code {
52 | KeyCode::Char('s') | KeyCode::Char('S') => {
53 | if player.is_playing() {
54 | player.pause();
55 | } else {
56 | player.resume();
57 | }
58 | return true;
59 | }
60 | _ => {
61 | return false;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/handler/player.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use crossterm::event::KeyCode;
19 |
20 | use crate::{app::App, media::player::Player};
21 |
22 | pub fn handle_player(app: &mut App, code: KeyCode) -> bool {
23 | match code {
24 | KeyCode::Char('-') => {
25 | let volume = app.player.volume() - 0.05;
26 | let new_volume = volume.max(0.0);
27 | app.player.set_volume(new_volume);
28 | return true;
29 | }
30 | KeyCode::Char('=') | KeyCode::Char('+') => {
31 | let volume = app.player.volume() + 0.05;
32 | let new_volume = volume.min(1.0);
33 | app.player.set_volume(new_volume);
34 | return true;
35 | }
36 | _ => {
37 | return false;
38 | }
39 | }
40 | }
41 |
42 | pub fn handle_radio(app: &mut App, code: KeyCode) -> bool {
43 | match code {
44 | KeyCode::Char('-') => {
45 | let volume = app.radio.volume() - 0.05;
46 | let new_volume = volume.max(0.0);
47 | app.radio.set_volume(new_volume);
48 | return true;
49 | }
50 | KeyCode::Char('=') | KeyCode::Char('+') => {
51 | let volume = app.radio.volume() + 0.05;
52 | let new_volume = volume.min(1.0);
53 | app.radio.set_volume(new_volume);
54 | return true;
55 | }
56 | _ => {
57 | return false;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/handler/radio.rs:
--------------------------------------------------------------------------------
1 | use crossterm::event::KeyCode;
2 |
3 | use crate::{
4 | app::App,
5 | media::{
6 | media::{Media, Source},
7 | player::Player,
8 | },
9 | };
10 |
11 | pub fn handle_radio_fs(app: &mut App, code: KeyCode) -> bool {
12 | let rfs = &mut app.radio_fs;
13 | let list = &rfs.radios;
14 | if list.is_empty() {
15 | return false;
16 | }
17 | match code {
18 | KeyCode::Up => {
19 | let curr_index = rfs.index.selected().unwrap();
20 | if curr_index == 0 {
21 | rfs.index.select(Some(list.len() - 1));
22 | } else {
23 | rfs.index.select(Some(curr_index - 1));
24 | }
25 | true
26 | }
27 | KeyCode::Down => {
28 | let curr_index = rfs.index.selected().unwrap();
29 | if curr_index == list.len() - 1 {
30 | rfs.index.select(Some(0));
31 | } else {
32 | rfs.index.select(Some(curr_index + 1));
33 | }
34 | true
35 | }
36 | KeyCode::Enter => {
37 | let s = rfs.index.selected();
38 | if let Some(selected_index) = s {
39 | if app.player.is_playing() {
40 | app.player.pause();
41 | }
42 | app.radio.add_to_list(
43 | Media {
44 | src: Source::M3u8(rfs.radios[selected_index].clone()),
45 | },
46 | true,
47 | );
48 | }
49 |
50 | true
51 | }
52 | _ => false,
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use app::*;
19 |
20 | use ui::*;
21 | use util::*;
22 |
23 | mod app;
24 | mod config;
25 | mod handler;
26 | mod media;
27 | mod ui;
28 | mod util;
29 |
30 | fn main() {
31 | let mut app = App::new().unwrap();
32 | match app.run() {
33 | Ok(_) => {}
34 | Err(e) => {
35 | println!("{:?}", e);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/media/media.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use crate::ui::radio::RadioConfig;
19 |
20 | pub enum Source {
21 | M3u8(RadioConfig),
22 | Local(String),
23 | }
24 |
25 | pub struct Media {
26 | pub src: Source,
27 | }
28 |
--------------------------------------------------------------------------------
/src/media/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | pub mod media;
19 | pub mod player;
20 |
--------------------------------------------------------------------------------
/src/media/player.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::cmp::max;
19 |
20 | use std::sync::mpsc::{Receiver, Sender};
21 | use std::{
22 | fs::File,
23 | io::{BufReader, Write},
24 | ops::Add,
25 | path::Path,
26 | sync::mpsc::channel,
27 | thread,
28 | time::{Duration, Instant, SystemTime},
29 | };
30 |
31 | use m3u8_rs::{MediaPlaylist, Playlist};
32 |
33 | use rodio::cpal;
34 | use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source};
35 | use tui::widgets::ListState;
36 |
37 | use crate::util::lyrics::Lyrics;
38 | use crate::util::m3u8::empty_cache;
39 | use crate::{m3u8::download_m3u8_playlist, util::net::download_as_bytes};
40 |
41 | use super::media::Media;
42 |
43 | #[derive(PartialEq, Eq, PartialOrd, Ord)]
44 | pub enum PlayStatus {
45 | Waiting,
46 | Playing(Instant, Duration),
47 | // elapsed, times already played
48 | Stopped(Duration), // times already played
49 | }
50 |
51 | pub struct PlayListItem {
52 | pub name: String,
53 | pub duration: Duration,
54 | pub current_pos: Duration,
55 | pub status: PlayStatus,
56 | pub path: String,
57 | pub lyrics: Lyrics,
58 | pub lyrics_index: ListState,
59 | }
60 |
61 | pub struct PlayList {
62 | pub lists: Vec,
63 | }
64 |
65 | pub trait Player {
66 | // 初始化
67 | fn new() -> Self;
68 |
69 | // 添加歌曲
70 | fn add_to_list(&mut self, media: Media, once: bool) -> bool;
71 |
72 | // 播放
73 | fn play(&mut self) -> bool;
74 |
75 | // 下一首
76 | fn next(&mut self) -> bool;
77 |
78 | // 停止
79 | fn stop(&mut self) -> bool;
80 |
81 | // 暂停
82 | fn pause(&mut self) -> bool;
83 |
84 | // 继续
85 | fn resume(&mut self) -> bool;
86 |
87 | // 播放进度
88 | fn get_progress(&self) -> (f32, f32);
89 |
90 | // 是否正在播放
91 | fn is_playing(&self) -> bool;
92 |
93 | // 提供一个接口,用于更新player状态
94 | fn tick(&mut self);
95 |
96 | // 当前歌词
97 | fn current_lyric(&self) -> &str;
98 |
99 | // 有歌词
100 | fn has_lyrics(&self) -> bool;
101 |
102 | // 音量
103 | fn volume(&self) -> f32;
104 |
105 | // 设置音量
106 | fn set_volume(&mut self, new_volume: f32) -> bool;
107 | }
108 |
109 | pub struct MusicPlayer {
110 | // params
111 | pub current_time: Duration,
112 | pub total_time: Duration,
113 | pub play_list: PlayList,
114 | // media: Media,
115 | // stream
116 | stream: OutputStream,
117 | stream_handle: OutputStreamHandle,
118 | sink: Sink,
119 | current_lyric: Option,
120 | initialized: bool,
121 | }
122 |
123 | impl Player for MusicPlayer {
124 | fn new() -> Self {
125 | for dev in cpal::available_hosts() {
126 | println!("{:?}", dev);
127 | }
128 | let (stream, stream_handle) = OutputStream::try_default().unwrap();
129 | let sink = Sink::try_new(&stream_handle).unwrap();
130 | Self {
131 | current_time: Duration::from_secs(0),
132 | total_time: Duration::from_secs(0),
133 | play_list: PlayList { lists: vec![] },
134 | // media: f,
135 | stream,
136 | stream_handle,
137 | sink,
138 | current_lyric: None,
139 | initialized: false,
140 | }
141 | }
142 |
143 | fn add_to_list(&mut self, media: Media, once: bool) -> bool {
144 | match media.src {
145 | super::media::Source::Local(path) => {
146 | return self.play_with_file(path, once);
147 | }
148 | super::media::Source::M3u8(_path) => false,
149 | }
150 | }
151 |
152 | // fn next(&mut self) -> bool {
153 | // // self._sink.
154 | // true
155 | // }
156 |
157 | fn play(&mut self) -> bool {
158 | self.sink.play();
159 | if let Some(item) = self.play_list.lists.first_mut() {
160 | let status = &mut item.status;
161 | match status {
162 | PlayStatus::Waiting => {
163 | *status = PlayStatus::Playing(Instant::now(), Duration::from_nanos(0));
164 | }
165 | PlayStatus::Playing(_, _) => {}
166 | PlayStatus::Stopped(duration) => {
167 | *status = PlayStatus::Playing(Instant::now(), *duration);
168 | }
169 | }
170 | }
171 | true
172 | }
173 |
174 | fn next(&mut self) -> bool {
175 | let len = self.play_list.lists.len();
176 | if len >= 1 {
177 | self.play_list.lists.remove(0);
178 | self.stop();
179 | if !self.play_list.lists.is_empty() {
180 | // next song
181 | let top_music = self.play_list.lists.first().unwrap();
182 | let f = File::open(top_music.path.as_str()).unwrap();
183 | let buf_reader = BufReader::new(f);
184 | let (stream, stream_handle) = OutputStream::try_default().unwrap();
185 | self.stream = stream;
186 | self.stream_handle = stream_handle;
187 | let volume = self.volume();
188 | self.sink = Sink::try_new(&self.stream_handle).unwrap();
189 | self.set_volume(volume);
190 | self.sink.append(Decoder::new(buf_reader).unwrap());
191 | self.play();
192 | }
193 | // for
194 | } else {
195 | // no more sound to play
196 | return false;
197 | }
198 | true
199 | }
200 |
201 | fn stop(&mut self) -> bool {
202 | self.sink.stop();
203 | true
204 | }
205 |
206 | fn pause(&mut self) -> bool {
207 | self.sink.pause();
208 | if let Some(item) = self.play_list.lists.first_mut() {
209 | let status = &mut item.status;
210 | match status {
211 | PlayStatus::Waiting => {}
212 | PlayStatus::Playing(instant, duration) => {
213 | *status = PlayStatus::Stopped(duration.add(instant.elapsed()));
214 | }
215 | PlayStatus::Stopped(_) => {}
216 | }
217 | }
218 | true
219 | }
220 |
221 | fn resume(&mut self) -> bool {
222 | self.sink.play();
223 | if let Some(item) = self.play_list.lists.first_mut() {
224 | let status = &mut item.status;
225 | match status {
226 | PlayStatus::Waiting => {}
227 | PlayStatus::Playing(_, _) => {}
228 | PlayStatus::Stopped(duration) => {
229 | *status = PlayStatus::Playing(Instant::now(), *duration);
230 | }
231 | }
232 | }
233 | return true;
234 | }
235 |
236 | fn get_progress(&self) -> (f32, f32) {
237 | return (0.0, 0.0);
238 | }
239 |
240 | fn is_playing(&self) -> bool {
241 | return self.initialized && !self.sink.is_paused() && !self.play_list.lists.is_empty();
242 | }
243 |
244 | fn tick(&mut self) {
245 | let is_playing = self.is_playing();
246 | if let Some(song) = self.play_list.lists.first_mut() {
247 | let status = &mut song.status;
248 | match status {
249 | PlayStatus::Waiting => {
250 | if is_playing {
251 | *status = PlayStatus::Playing(Instant::now(), Duration::from_nanos(0));
252 | }
253 | }
254 | PlayStatus::Playing(instant, duration) => {
255 | let now = instant.elapsed().add(duration.clone());
256 | if now.ge(&song.duration) {
257 | // next song, delete 0
258 | self.next();
259 | } else {
260 | // update status
261 | self.current_time = now;
262 | self.total_time = song.duration.clone();
263 | // add lyrics
264 | let selected_index = song.lyrics_index.selected().unwrap();
265 | if selected_index + 1 < song.lyrics.list.len() {
266 | let next_lyric = &song.lyrics.list[selected_index + 1];
267 | if self.current_time > next_lyric.time {
268 | song.lyrics_index.select(Some(selected_index + 1));
269 | }
270 | }
271 | }
272 | }
273 | PlayStatus::Stopped(dur) => {
274 | self.current_time = dur.clone();
275 | self.total_time = song.duration.clone();
276 | }
277 | }
278 | } else {
279 | // stop player when no sounds
280 | if self.play_list.lists.is_empty() {
281 | self.stop();
282 | }
283 | }
284 | }
285 |
286 | fn current_lyric(&self) -> &str {
287 | if let Some(lyric) = &self.current_lyric {
288 | return lyric.as_str();
289 | } else {
290 | return "No Lyrics";
291 | }
292 | }
293 |
294 | fn has_lyrics(&self) -> bool {
295 | !self.play_list.lists.is_empty()
296 | && !self.play_list.lists.first().unwrap().lyrics.list.is_empty()
297 | }
298 |
299 | fn volume(&self) -> f32 {
300 | return self.sink.volume();
301 | }
302 |
303 | fn set_volume(&mut self, new_volume: f32) -> bool {
304 | self.sink.set_volume(new_volume);
305 | true
306 | }
307 | }
308 |
309 | impl MusicPlayer {
310 | pub fn playing_song(&self) -> Option<&PlayListItem> {
311 | return self.play_list.lists.first();
312 | }
313 |
314 | fn play_with_file(&mut self, path: String, once: bool) -> bool {
315 | let duration: Duration;
316 | if path.ends_with(".mp3") {
317 | let dur = mp3_duration::from_path(path.clone());
318 | match dur {
319 | Ok(dur) => {
320 | duration = dur;
321 | }
322 | Err(err) => {
323 | // EOF catch
324 | duration = err.at_duration;
325 | if duration.is_zero() {
326 | return false;
327 | }
328 | }
329 | }
330 | } else {
331 | if let Ok(f) = File::open(path.as_str()) {
332 | let dec = Decoder::new(f);
333 | if let Ok(dec) = dec {
334 | if let Some(dur) = dec.total_duration() {
335 | duration = dur;
336 | } else {
337 | return false;
338 | }
339 | } else {
340 | return false;
341 | }
342 | } else {
343 | return false;
344 | }
345 | }
346 | // find lyrics
347 | let lyrics = Lyrics::from_music_path(path.as_str());
348 | // open
349 | match File::open(path.as_str()) {
350 | Ok(f) => {
351 | let path = Path::new(path.as_str());
352 | let file_name = path.file_name().unwrap().to_string_lossy().to_string();
353 | // Result<(stream,streamHanlde),std::error:Error>
354 | if once || self.play_list.lists.is_empty() {
355 | // rebuild
356 | self.stop();
357 | let buf_reader = BufReader::new(f);
358 | let sink = self.stream_handle.play_once(buf_reader).unwrap();
359 | self.sink = sink;
360 | self.play_list.lists.clear();
361 | }
362 | let mut state = ListState::default();
363 | state.select(Some(0));
364 | self.play_list.lists.push(PlayListItem {
365 | name: file_name,
366 | duration,
367 | current_pos: Duration::from_secs(0),
368 | status: PlayStatus::Waiting,
369 | path: path.to_string_lossy().to_string(),
370 | lyrics,
371 | lyrics_index: state,
372 | });
373 | if !self.initialized {
374 | self.initialized = true;
375 | }
376 | self.play();
377 | self.tick();
378 | return true;
379 | }
380 | Err(_) => false,
381 | }
382 | }
383 | }
384 |
385 | impl Drop for MusicPlayer {
386 | fn drop(&mut self) {
387 | // println!()
388 | }
389 | }
390 |
391 | pub struct RadioItem {
392 | list: MediaPlaylist,
393 | url: String,
394 | }
395 |
396 | #[allow(dead_code)]
397 | pub struct RadioPlayer {
398 | pub item: Option,
399 | pub list: Vec,
400 | stream: OutputStream,
401 | stream_handle: OutputStreamHandle,
402 | sink: Sink,
403 | is_playing: bool,
404 | last_playing_id: i32,
405 | data_tx: Sender,
406 | data_rx: Receiver,
407 | elasped: SystemTime,
408 | gap: Duration,
409 | }
410 |
411 | impl Player for RadioPlayer {
412 | fn new() -> Self {
413 | let (stream, handle) = OutputStream::try_default().unwrap();
414 | let sink = Sink::try_new(&handle).unwrap();
415 | let (tx, rx) = channel();
416 | empty_cache();
417 | RadioPlayer {
418 | item: None,
419 | list: vec![],
420 | stream,
421 | stream_handle: handle,
422 | sink,
423 | is_playing: false,
424 | last_playing_id: -1,
425 | data_rx: rx,
426 | data_tx: tx,
427 | elasped: SystemTime::now(),
428 | gap: Duration::from_secs(5),
429 | }
430 | }
431 |
432 | fn add_to_list(&mut self, media: Media, _: bool) -> bool {
433 | self.last_playing_id = -1;
434 | let src = media.src;
435 | match src {
436 | super::media::Source::M3u8(url) => {
437 | let (tx, rx) = channel();
438 | let m3u8_url = url.url.clone();
439 | thread::spawn(move || {
440 | let playlist = download_m3u8_playlist(m3u8_url);
441 | tx.send(playlist).unwrap();
442 | });
443 | match rx.recv_timeout(Duration::from_secs(5)) {
444 | Ok(list) => {
445 | if let Ok(playlist) = list {
446 | match playlist {
447 | Playlist::MasterPlaylist(_) => {}
448 | Playlist::MediaPlaylist(pl) => {
449 | let item = RadioItem {
450 | list: pl,
451 | url: url.url.clone(),
452 | };
453 | self.item = Some(item);
454 | self.download_and_push();
455 | self.play();
456 | }
457 | }
458 | return true;
459 | }
460 | }
461 | Err(_err) => {
462 | // ignore
463 | }
464 | }
465 | false
466 | }
467 | super::media::Source::Local(_) => false,
468 | }
469 | }
470 |
471 | fn play(&mut self) -> bool {
472 | self.sink.play();
473 | self.is_playing = true;
474 | true
475 | }
476 |
477 | fn next(&mut self) -> bool {
478 | false
479 | }
480 |
481 | fn stop(&mut self) -> bool {
482 | self.sink.stop();
483 | true
484 | }
485 |
486 | fn pause(&mut self) -> bool {
487 | self.sink.pause();
488 | self.is_playing = false;
489 | true
490 | }
491 |
492 | fn resume(&mut self) -> bool {
493 | self.sink.play();
494 | self.is_playing = true;
495 | true
496 | }
497 |
498 | fn get_progress(&self) -> (f32, f32) {
499 | return (0.0, 0.0);
500 | }
501 |
502 | fn is_playing(&self) -> bool {
503 | self.is_playing
504 | }
505 |
506 | fn tick(&mut self) {
507 | match self.data_rx.try_recv() {
508 | Ok(data) => {
509 | // let f = File::open("D:\\audio.wav").unwrap();
510 | let mut cache_dir = dirs::cache_dir().unwrap();
511 | cache_dir.push("RustPlayer");
512 | std::fs::create_dir_all(cache_dir.clone()).unwrap();
513 | let timestamp = SystemTime::now()
514 | .duration_since(SystemTime::UNIX_EPOCH)
515 | .unwrap();
516 | let fname = timestamp.as_nanos().to_string();
517 | cache_dir.push(fname.as_str());
518 | let mut f = File::create(cache_dir.clone()).unwrap();
519 | f.write_all(data.as_ref()).unwrap();
520 | let decoder = ffmpeg_decoder::Decoder::open(cache_dir);
521 | // let buffer = BufReader::new(f);
522 | // let decoder = rodio::Decoder::new_mp4(buffer, rodio::decoder::Mp4Type::M4a);
523 | // Decoder::
524 | match decoder {
525 | Ok(dec) => {
526 | self.sink.append(dec);
527 | }
528 | Err(err) => {
529 | eprintln!("{:?}", err);
530 | }
531 | }
532 | }
533 | Err(_) => {}
534 | }
535 | // check length
536 | if let Ok(elapsed) = self.elasped.elapsed() {
537 | // 过了gap时间重新拉取m3u8
538 | if self.is_playing() && elapsed.as_secs() > self.gap.as_secs() {
539 | self.download_and_push();
540 | }
541 | }
542 | }
543 |
544 | fn current_lyric(&self) -> &str {
545 | return "";
546 | }
547 |
548 | fn has_lyrics(&self) -> bool {
549 | false
550 | }
551 |
552 | fn volume(&self) -> f32 {
553 | self.sink.volume()
554 | }
555 |
556 | fn set_volume(&mut self, new_volume: f32) -> bool {
557 | self.sink.set_volume(new_volume);
558 | true
559 | }
560 | }
561 |
562 | impl RadioPlayer {
563 | /// 触发下载
564 | fn download_and_push(&mut self) {
565 | self.elasped = SystemTime::now();
566 | // 第一次下载,直接全部下载
567 | if self.last_playing_id == -1 {
568 | // 直接全部下载
569 | let item = &self.item;
570 | if let Some(radio) = item {
571 | let index = radio.url.clone().rfind("/").unwrap();
572 | let base_url = radio.url.as_str()[0..index + 1].to_string();
573 | let tx_clone = self.data_tx.clone();
574 | let urls: Vec = radio
575 | .list
576 | .segments
577 | .iter()
578 | .map(|e| e.uri.clone())
579 | .map(|uri| base_url.clone() + &uri)
580 | .collect();
581 | self.last_playing_id =
582 | radio.list.media_sequence + max((radio.list.segments.len() - 1) as i32, 0);
583 | thread::spawn(move || {
584 | for url in urls {
585 | match download_as_bytes(url.as_str(), &tx_clone) {
586 | _ => {
587 | // println!("{:?} downloaded",url);
588 | }
589 | }
590 | }
591 | });
592 | }
593 | } else {
594 | // 更新playlist列表
595 | if let Some(radio) = &self.item {
596 | let index = radio.url.clone().rfind("/").unwrap();
597 | let base_url = radio.url.as_str()[0..index + 1].to_string();
598 | match download_m3u8_playlist(radio.url.clone()) {
599 | Ok(playlist) => match playlist {
600 | Playlist::MasterPlaylist(_) => todo!(),
601 | Playlist::MediaPlaylist(media_playlist) => {
602 | let seq = media_playlist.media_sequence;
603 | let skip_num = max(self.last_playing_id - seq + 1, 0) as usize;
604 | let segs = &media_playlist.segments[skip_num..];
605 | // println!("add {:?}",segs);
606 | let urls: Vec = segs
607 | .iter()
608 | .map(|e| e.uri.clone())
609 | .map(|uri| base_url.clone() + &uri)
610 | .collect();
611 | self.last_playing_id =
612 | seq + max((media_playlist.segments.len() - 1) as i32, 0);
613 | let tx_clone = self.data_tx.clone();
614 | thread::spawn(move || {
615 | for url in urls {
616 | match download_as_bytes(url.as_str(), &tx_clone) {
617 | _ => {
618 | // println!("update {:?}",url);
619 | }
620 | }
621 | }
622 | });
623 | }
624 | },
625 | Err(_) => {
626 | // ignore
627 | }
628 | }
629 | }
630 | }
631 | }
632 | }
633 |
--------------------------------------------------------------------------------
/src/ui/effects.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::{
19 | sync::{Arc, RwLock},
20 | vec,
21 | };
22 |
23 | use rand::Rng;
24 | use tui::{
25 | backend::Backend,
26 | layout::{Alignment, Rect},
27 | style::{Color, Modifier, Style},
28 | widgets::{BarChart, Block, BorderType, Borders, List, ListItem},
29 | Frame,
30 | };
31 |
32 | use crate::{app::App, media::player::Player};
33 | use lazy_static::lazy_static;
34 |
35 | pub struct WaveEffectCache {
36 | cols_data: Vec<(&'static str, u64)>,
37 | }
38 |
39 | lazy_static! {
40 | pub static ref LAST_EFFECT_TIME: Arc> =
41 | Arc::new(RwLock::new(std::time::Instant::now()));
42 | pub static ref CACHE_WAVE: Arc>> = Arc::new(RwLock::new(None));
43 | }
44 |
45 | pub fn draw_bar_charts_effect(app: &mut App, frame: &mut Frame, area: Rect)
46 | where
47 | B: Backend,
48 | {
49 | let player = &mut app.player;
50 | let radio = &app.radio;
51 | match player.has_lyrics() {
52 | true => {
53 | let mut lyrics = vec![];
54 | if let Some(item) = player.play_list.lists.first_mut() {
55 | for ele in &item.lyrics.list {
56 | lyrics.push(ListItem::new(ele.content.as_str()));
57 | }
58 | let list = List::new(lyrics)
59 | .highlight_symbol("*")
60 | .highlight_style(
61 | Style::default()
62 | .add_modifier(Modifier::BOLD)
63 | .bg(Color::Cyan),
64 | )
65 | .block(
66 | Block::default()
67 | .borders(Borders::ALL)
68 | .border_type(BorderType::Rounded)
69 | .title("Lyrics")
70 | .title_alignment(Alignment::Center),
71 | );
72 | frame.render_stateful_widget(list, area, &mut item.lyrics_index);
73 | }
74 | }
75 | false => {
76 | let dur = LAST_EFFECT_TIME.read().unwrap().elapsed();
77 | let cache_wave = CACHE_WAVE.read().unwrap();
78 | if dur.as_millis() <= 300 && cache_wave.is_some() {
79 | // reuse draw on every 300ms.
80 | let items = BarChart::default()
81 | .bar_width(4)
82 | .bar_gap(1)
83 | .bar_style(Style::default().fg(Color::Cyan).bg(Color::Black))
84 | .data(&cache_wave.as_ref().unwrap().cols_data)
85 | .value_style(Style::default().add_modifier(Modifier::ITALIC))
86 | .label_style(Style::default().add_modifier(Modifier::ITALIC))
87 | .max(10)
88 | .block(
89 | Block::default()
90 | .borders(Borders::ALL)
91 | .border_type(BorderType::Rounded)
92 | .title("Wave")
93 | .title_alignment(Alignment::Center),
94 | );
95 | frame.render_widget(items, area);
96 | } else {
97 | drop(cache_wave);
98 | let mut rng = rand::thread_rng();
99 | let mut cols = vec![];
100 | for _ in 0..20 {
101 | let mut i = rng.gen_range(0..10);
102 | if !player.is_playing() && !radio.is_playing() {
103 | i = 0
104 | }
105 | cols.push(("_", i))
106 | }
107 | let items = BarChart::default()
108 | .bar_width(4)
109 | .bar_gap(1)
110 | .bar_style(Style::default().fg(Color::Cyan).bg(Color::Black))
111 | .data(&cols)
112 | .value_style(Style::default().add_modifier(Modifier::ITALIC))
113 | .label_style(Style::default().add_modifier(Modifier::ITALIC))
114 | .max(10)
115 | .block(
116 | Block::default()
117 | .borders(Borders::ALL)
118 | .border_type(BorderType::Rounded)
119 | .title("Wave")
120 | .title_alignment(Alignment::Center),
121 | );
122 | frame.render_widget(items, area);
123 | *LAST_EFFECT_TIME.write().unwrap() = std::time::Instant::now();
124 | *CACHE_WAVE.write().unwrap() = Some(WaveEffectCache { cols_data: cols });
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/ui/frame.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 |
--------------------------------------------------------------------------------
/src/ui/fs.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::env::current_dir;
19 | use std::fmt::Debug;
20 | use std::fs;
21 | use std::fs::DirEntry;
22 | use std::path::Path;
23 |
24 | use failure::{Error, Fail};
25 | use tui::backend::Backend;
26 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
27 | use tui::style::{Color, Style};
28 | use tui::text::Text;
29 | use tui::widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph, Wrap};
30 | use tui::Frame;
31 |
32 | use crate::app::ActiveModules;
33 | use crate::App;
34 |
35 | #[allow(dead_code)]
36 | pub struct FsExplorer {
37 | pub current_path: String,
38 | pub files: Vec,
39 | pub dirs: Vec,
40 | pub index: ListState,
41 | on_error_msg_callback: Option,
42 | accept_suffix: Vec<&'static str>,
43 | }
44 |
45 | #[derive(Fail, Debug)]
46 | #[fail(display = "FsError: {}", msg)]
47 | pub struct FsError {
48 | msg: &'static str,
49 | }
50 |
51 | impl FsExplorer {
52 | pub fn default(callback: Option) -> Result {
53 | let path = current_dir()?;
54 | let path_str = path.to_str().ok_or(FsError {
55 | msg: "path to_str error.",
56 | })?;
57 | let mut list_state = ListState::default();
58 | list_state.select(Some(0));
59 | let mut exp = Self {
60 | current_path: path_str.to_string(),
61 | files: vec![],
62 | dirs: vec![],
63 | index: list_state,
64 | on_error_msg_callback: callback,
65 | accept_suffix: vec!["mp3", "wav", "flac", "ts"],
66 | };
67 | let (dirs, files) = exp.visit_dir(path_str)?;
68 | exp.files = files;
69 | exp.dirs = dirs;
70 | Ok(exp)
71 | }
72 |
73 | pub fn refresh(&mut self) {
74 | let str = String::from(self.current_path.as_str());
75 | match self.visit_dir(str.as_str()) {
76 | Ok(entries) => {
77 | self.dirs = entries.0;
78 | self.files = entries.1;
79 | }
80 | Err(_) => {}
81 | }
82 | }
83 |
84 | /// dirs,mp3s
85 | fn visit_dir(&mut self, path: &str) -> Result<(Vec, Vec), Error> {
86 | let path = Path::new(path);
87 | let mut dir_entries = vec![];
88 | let mut file_entries = vec![];
89 | match path.is_dir() {
90 | true => {
91 | for entry in fs::read_dir(path)? {
92 | match entry {
93 | Ok(entry) => {
94 | for accept_suffix in self.accept_suffix.iter() {
95 | let path = entry.path();
96 | // println!("{:?}", path.display());
97 | if path.is_dir() {
98 | dir_entries.push(entry);
99 | break;
100 | } else if let Some(ext) = path.extension() {
101 | if ext.to_string_lossy().ends_with(accept_suffix) {
102 | file_entries.push(entry);
103 | break;
104 | }
105 | }
106 | }
107 | }
108 | Err(_) => {
109 | continue;
110 | }
111 | }
112 | }
113 | }
114 | false => {
115 | return Err(Error::from(FsError {
116 | msg: "is not a valid path",
117 | }));
118 | }
119 | }
120 | dir_entries.sort_by(|item1, item2| {
121 | return item1.file_name().cmp(&item2.file_name());
122 | });
123 | file_entries.sort_by(|item1, item2| {
124 | return item1.file_name().cmp(&item2.file_name());
125 | });
126 | Ok((dir_entries, file_entries))
127 | }
128 | }
129 |
130 | fn draw_dir_item(entry: &DirEntry, vec: &mut Vec) {
131 | let file_name = "📂".to_owned() + &String::from(entry.file_name().to_str().unwrap()) + "/";
132 | vec.push(ListItem::new(file_name));
133 | }
134 |
135 | fn draw_file_item(entry: &DirEntry, vec: &mut Vec) {
136 | let file_name = "🎵".to_owned() + &String::from(entry.file_name().to_str().unwrap());
137 | vec.push(ListItem::new(file_name));
138 | }
139 |
140 | pub fn draw_fs_tree(app: &mut App, frame: &mut Frame, area: Rect)
141 | where
142 | B: Backend,
143 | {
144 | let fse = &mut app.fs;
145 | let fs_chunks = Layout::default()
146 | .direction(Direction::Vertical)
147 | .constraints([Constraint::Length(3), Constraint::Percentage(100)])
148 | .split(area);
149 |
150 | let folder = Paragraph::new(Text::from(fse.current_path.as_str()))
151 | .wrap(Wrap { trim: true })
152 | .alignment(Alignment::Center)
153 | .block(
154 | Block::default()
155 | .title("Current Folder")
156 | .title_alignment(Alignment::Center)
157 | .border_type(BorderType::Rounded)
158 | .borders(Borders::ALL),
159 | );
160 | frame.render_widget(folder, fs_chunks[0]);
161 | // list
162 | let mut items = vec![ListItem::new("🔙Go Back")];
163 | for entry in &fse.dirs {
164 | draw_dir_item(entry, &mut items);
165 | }
166 | for entry in &fse.files {
167 | draw_file_item(entry, &mut items);
168 | }
169 | let mut blk = Block::default()
170 | .title("Explorer")
171 | .title_alignment(Alignment::Center)
172 | .borders(Borders::ALL)
173 | .border_type(BorderType::Rounded);
174 | if app.active_modules == ActiveModules::Fs {
175 | blk = blk.border_style(Style::default().fg(Color::Cyan));
176 | }
177 | let file_list = List::new(items)
178 | .block(blk)
179 | .highlight_style(Style::default().bg(Color::Cyan))
180 | .highlight_symbol("> ");
181 | frame.render_stateful_widget(file_list, fs_chunks[1], &mut fse.index);
182 | }
183 |
--------------------------------------------------------------------------------
/src/ui/help.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::vec;
19 |
20 | use tui::{
21 | backend::Backend,
22 | layout::{Constraint, Direction, Layout, Rect},
23 | style::{Color, Style},
24 | widgets::{Block, BorderType, Borders, Paragraph, Row, Table},
25 | Frame,
26 | };
27 |
28 | use crate::app::App;
29 |
30 | pub fn draw_help(_app: &mut App, frame: &mut Frame, area: Rect)
31 | where
32 | B: Backend,
33 | {
34 | let chunks = Layout::default()
35 | .direction(Direction::Vertical)
36 | .constraints([Constraint::Length(3), Constraint::Percentage(100)])
37 | .split(area);
38 | let homepage_text =
39 | Paragraph::new("Press key to open author(KetaNetwork)'s home page.").block(
40 | Block::default()
41 | .borders(Borders::ALL)
42 | .border_type(BorderType::Rounded),
43 | );
44 | frame.render_widget(homepage_text, chunks[0]);
45 |
46 | let help_table = Table::new([
47 | Row::new(["h", "open or close this help."]),
48 | Row::new([
49 | "Tab",
50 | "switch highlight block. (Audio Explorer/Radio Explorer)",
51 | ]),
52 | Row::new(["r", "open radio config list."]),
53 | Row::new(["->", "add audio to play list."]),
54 | Row::new([
55 | "Enter",
56 | "play audio immediately and clean play list or enter selected folder.",
57 | ]),
58 | Row::new(["-/+", "decrease/increase volume."]),
59 | Row::new(["s", "pause/resume audio playback."]),
60 | Row::new(["n", "play the next audio."]),
61 | Row::new(["q", "quit RustPlayer."]),
62 | Row::new(["↑/↓", "change selected index."]),
63 | ])
64 | .header(
65 | Row::new(vec!["Key", "Function"])
66 | .style(Style::default().fg(Color::White))
67 | .bottom_margin(1),
68 | )
69 | .block(
70 | Block::default()
71 | .title("Help Table")
72 | .border_type(BorderType::Rounded)
73 | .borders(Borders::ALL),
74 | )
75 | .column_spacing(2)
76 | .widths(&[Constraint::Min(6), Constraint::Percentage(100)]);
77 | frame.render_widget(help_table, chunks[1]);
78 | }
79 |
--------------------------------------------------------------------------------
/src/ui/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | pub mod effects;
19 | pub mod fs;
20 | pub mod help;
21 | pub mod music_board;
22 | pub mod play_list;
23 | pub mod progress;
24 | pub mod radio;
25 |
26 | pub enum EventType {
27 | Player,
28 | Radio,
29 | }
30 |
--------------------------------------------------------------------------------
/src/ui/music_board.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use tui::{
19 | backend::Backend,
20 | layout::{Alignment, Constraint, Direction, Layout, Rect},
21 | style::{Color, Modifier, Style},
22 | symbols::{self},
23 | text::Spans,
24 | widgets::{Block, BorderType, Borders, LineGauge, ListState, Paragraph},
25 | Frame,
26 | };
27 |
28 | use crate::{app::App, media::player::Player};
29 |
30 | use super::{effects::draw_bar_charts_effect, play_list::draw_play_list, progress::draw_progress};
31 |
32 | pub struct MusicController {
33 | pub state: ListState,
34 | }
35 |
36 | pub fn draw_music_board(app: &mut App, frame: &mut Frame, area: Rect)
37 | where
38 | B: Backend,
39 | {
40 | let main_layout_chunks = Layout::default()
41 | .direction(Direction::Vertical)
42 | .constraints([
43 | Constraint::Length(3),
44 | Constraint::Percentage(80),
45 | Constraint::Percentage(20),
46 | ])
47 | .split(area);
48 |
49 | draw_header(app, frame, main_layout_chunks[0]);
50 |
51 | let mid_layout_chunks = Layout::default()
52 | .direction(Direction::Horizontal)
53 | .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
54 | .split(main_layout_chunks[1]);
55 |
56 | draw_bar_charts_effect(app, frame, mid_layout_chunks[0]);
57 | draw_play_list(app, frame, mid_layout_chunks[1]);
58 | draw_progress(app, frame, main_layout_chunks[2]);
59 | }
60 |
61 | pub fn draw_header(app: &mut App, frame: &mut Frame, area: Rect)
62 | where
63 | B: Backend,
64 | {
65 | let player = &app.player;
66 | let main_layout_chunks = Layout::default()
67 | .direction(Direction::Horizontal)
68 | .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
69 | .split(area);
70 |
71 | let playing_text;
72 | if let Some(item) = player.playing_song() {
73 | playing_text = String::from(item.name.as_str());
74 | } else {
75 | playing_text = String::from("None");
76 | }
77 | let text = Paragraph::new(playing_text)
78 | .block(
79 | Block::default()
80 | .borders(Borders::ALL)
81 | .border_type(BorderType::Rounded)
82 | .title("Now Playing")
83 | .title_alignment(Alignment::Center),
84 | )
85 | .style(Style::default().add_modifier(Modifier::SLOW_BLINK));
86 |
87 | let sub_layout = Layout::default()
88 | .direction(Direction::Horizontal)
89 | .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
90 | .split(main_layout_chunks[0]);
91 |
92 | let sound_volume_percent = app.player.volume();
93 | let bar = LineGauge::default()
94 | .ratio(sound_volume_percent.into())
95 | .label("VOL")
96 | .line_set(symbols::line::THICK)
97 | .block(
98 | Block::default()
99 | .border_type(BorderType::Rounded)
100 | .borders(Borders::ALL),
101 | )
102 | .gauge_style(
103 | Style::default()
104 | .fg(Color::LightCyan)
105 | .bg(Color::Black)
106 | .add_modifier(Modifier::BOLD),
107 | );
108 |
109 | frame.render_widget(text, sub_layout[0]);
110 | frame.render_widget(bar, sub_layout[1]);
111 | let mut p = Paragraph::new(vec![Spans::from("▶(s) >>|(n) EXT(q) HLP(h)")])
112 | .style(Style::default())
113 | .alignment(Alignment::Center);
114 | if player.is_playing() {
115 | p = Paragraph::new(vec![Spans::from("||(s) >>|(n) EXT(q) HELP(h)")])
116 | .alignment(Alignment::Center);
117 | }
118 | let blk = Block::default()
119 | .borders(Borders::ALL)
120 | .title("Panel")
121 | .border_type(BorderType::Rounded)
122 | .title_alignment(Alignment::Center);
123 |
124 | // if app.active_modules == ActiveModules::MusicController {
125 | // blk = blk.border_style(Style::default().fg(Color::Cyan));
126 | // }
127 | p = p.block(blk);
128 | frame.render_widget(p, main_layout_chunks[1]);
129 | }
130 |
--------------------------------------------------------------------------------
/src/ui/play_list.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use tui::{
19 | backend::Backend,
20 | layout::{Alignment, Rect},
21 | widgets::{Block, BorderType, Borders, List, ListItem},
22 | Frame,
23 | };
24 |
25 | use crate::app::App;
26 |
27 | pub fn draw_play_list(app: &mut App, frame: &mut Frame, area: Rect)
28 | where
29 | B: Backend,
30 | {
31 | let mut items = vec![];
32 | let player = &app.player;
33 | for item in &player.play_list.lists {
34 | items.push(ListItem::new(item.name.as_str()))
35 | }
36 | let list = List::new(items).block(
37 | Block::default()
38 | .borders(Borders::ALL)
39 | .title("Playlist")
40 | .border_type(BorderType::Rounded)
41 | .title_alignment(Alignment::Center),
42 | );
43 | frame.render_widget(list, area);
44 | }
45 |
--------------------------------------------------------------------------------
/src/ui/progress.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use tui::{
19 | backend::Backend,
20 | layout::{Constraint, Layout, Rect},
21 | style::{Color, Modifier, Style},
22 | symbols::{self},
23 | widgets::LineGauge,
24 | Frame,
25 | };
26 |
27 | use crate::{app::App, media::player::Player};
28 |
29 | pub fn draw_progress(app: &mut App, frame: &mut Frame, area: Rect)
30 | where
31 | B: Backend,
32 | {
33 | let player = &app.player;
34 |
35 | let current_time = player.current_time;
36 | let total_time = player.total_time;
37 |
38 | let minute_mins = current_time.as_secs() / 60;
39 | let minute_secs = current_time.as_secs() % 60;
40 |
41 | let total_mins = total_time.as_secs() / 60;
42 | let total_secs = total_time.as_secs() % 60;
43 | let mut percent = 0.0;
44 | if total_time.as_secs() != 0 {
45 | percent = if player.is_playing() {
46 | current_time.as_secs_f64() / total_time.as_secs_f64()
47 | } else {
48 | 0.0
49 | };
50 | }
51 | let s = if player.is_playing() {
52 | format!(
53 | "{:0>2}:{:0>2} / {:0>2}:{:0>2}",
54 | minute_mins, minute_secs, total_mins, total_secs
55 | )
56 | } else {
57 | "No More Sound".to_string()
58 | };
59 |
60 | let gauge = LineGauge::default()
61 | .ratio(percent)
62 | .line_set(symbols::line::THICK)
63 | .label(s)
64 | .style(Style::default().add_modifier(Modifier::ITALIC))
65 | .gauge_style(
66 | Style::default()
67 | .bg(Color::DarkGray)
68 | .fg(Color::Cyan)
69 | .add_modifier(Modifier::BOLD),
70 | );
71 | let layout = Layout::default()
72 | .horizontal_margin(1)
73 | .constraints([Constraint::Percentage(100)].as_ref())
74 | .split(area);
75 | frame.render_widget(gauge, layout[0]);
76 | }
77 |
--------------------------------------------------------------------------------
/src/ui/radio.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::File,
3 | io::{BufRead, BufReader},
4 | vec,
5 | };
6 |
7 | use dirs;
8 | use tui::{
9 | backend::Backend,
10 | layout::{Alignment, Rect},
11 | style::{Color, Style},
12 | widgets::{Block, BorderType, Borders, List, ListItem, ListState},
13 | Frame,
14 | };
15 |
16 | use crate::app::App;
17 |
18 | #[derive(Clone)]
19 | pub struct RadioConfig {
20 | pub name: String,
21 | pub url: String,
22 | }
23 |
24 | pub struct RadioExplorer {
25 | pub radios: Vec,
26 | pub index: ListState,
27 | }
28 |
29 | impl RadioExplorer {
30 | pub fn new() -> Self {
31 | let mut config_dir = dirs::config_dir().unwrap();
32 | let mut configs = vec![];
33 | config_dir.push("RustPlayer");
34 | std::fs::create_dir_all(config_dir.clone()).unwrap();
35 | config_dir.push("radio.ini");
36 | let f: File;
37 | if !config_dir.as_path().exists() {
38 | File::create(config_dir.clone()).unwrap();
39 | }
40 | f = File::open(config_dir).unwrap();
41 | let reader = BufReader::new(f);
42 | let mut lines = reader.lines().map(|i| i.unwrap());
43 | while let Some(line) = lines.next() {
44 | if line.is_empty() {
45 | continue;
46 | }
47 | let radio_bean: Vec<_> = line.split(' ').collect();
48 | let config = RadioConfig {
49 | name: radio_bean[0].to_string(),
50 | url: radio_bean[1].to_string(),
51 | };
52 | configs.push(config);
53 | }
54 | let mut state = ListState::default();
55 | state.select(Some(0));
56 | Self {
57 | radios: configs,
58 | index: state,
59 | }
60 | }
61 | }
62 |
63 | pub fn draw_radio_list(app: &mut App, frame: &mut Frame, area: Rect)
64 | where
65 | B: Backend,
66 | {
67 | let fs = &mut app.radio_fs;
68 | let mut item_vec = vec![];
69 | for radio in &fs.radios {
70 | item_vec.push(ListItem::new(radio.name.as_str()));
71 | }
72 | let list = List::new(item_vec)
73 | .block(
74 | Block::default()
75 | .borders(Borders::all())
76 | .title("Radio List")
77 | .border_type(BorderType::Rounded)
78 | .border_style(Style::default().fg(Color::Cyan))
79 | .title_alignment(Alignment::Center),
80 | )
81 | .highlight_style(Style::default().bg(Color::Cyan))
82 | .highlight_symbol("> ");
83 | frame.render_stateful_widget(list, area, &mut fs.index);
84 | }
85 |
--------------------------------------------------------------------------------
/src/util/lyrics.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | use std::{fmt::Display, fs::File, io::Read, path::PathBuf, time::Duration, vec};
19 |
20 | use regex::Regex;
21 |
22 | pub struct Lyrics {
23 | pub list: Vec,
24 | }
25 |
26 | impl Display for Lyrics {
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 | // for item in self.list {
29 | // writeln!(f,"{:?}: {}",item.time,item.content)
30 | // }
31 | writeln!(f, "Lyrics Count: {}", self.list.len())
32 | }
33 | }
34 |
35 | pub struct Lyric {
36 | pub time: Duration,
37 | pub content: String,
38 | }
39 |
40 | impl Lyrics {
41 | pub fn from_music_path(s: &str) -> Self {
42 | // change to *.lrc
43 | let mut p = PathBuf::from(s);
44 | p.set_extension("lrc");
45 | let f = File::open(p);
46 | match f {
47 | Ok(mut f) => {
48 | return Lyrics::from_read(&mut f);
49 | }
50 | Err(_) => return Self { list: vec![] },
51 | }
52 | }
53 |
54 | pub fn from_read(f: &mut File) -> Self {
55 | let mut buffer = vec![];
56 | let regex =
57 | Regex::new(r"\[(?P\d+):(?P\d+).(?P\d+)](?P[^\[\]]*)").unwrap();
58 | f.read_to_end(&mut buffer).unwrap();
59 | let m = String::from_utf8(buffer).unwrap();
60 | let mut lyrics_vec = vec![];
61 | for cap in regex.captures_iter(m.as_str()) {
62 | let min = cap["min"].parse::().unwrap();
63 | let sec = cap["sec"].parse::().unwrap();
64 | let ms = cap["ms"].parse::().unwrap();
65 | let dur = Duration::from_millis(ms + sec * 1000 + min * 1000 * 60);
66 | lyrics_vec.push(Lyric {
67 | time: dur,
68 | content: String::from(&cap["content"]),
69 | });
70 | }
71 | Self { list: lyrics_vec }
72 | }
73 |
74 | #[allow(dead_code)]
75 | pub fn count(&self) -> usize {
76 | self.list.len()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/util/m3u8.rs:
--------------------------------------------------------------------------------
1 | use std::fs::read_dir;
2 | use std::sync::mpsc;
3 |
4 | use std::thread;
5 | use std::time::Duration;
6 |
7 | use failure::format_err;
8 | use m3u8_rs::Playlist;
9 |
10 | use crate::net::download;
11 |
12 | pub fn download_m3u8_playlist(url: String) -> Result {
13 | let (tx, rx) = mpsc::channel();
14 | thread::spawn(move || download(url.as_str(), &tx));
15 | let resp = rx.recv_timeout(Duration::from_secs(5));
16 | return if let Ok(data) = resp {
17 | let playlist = m3u8_rs::parse_playlist(data.as_bytes());
18 | match playlist {
19 | Ok(list) => Ok(list.1),
20 | Err(_err) => Err(format_err!("Parse Playlist Failed")),
21 | }
22 | } else {
23 | println!("{:?}", resp);
24 | Err(format_err!("Download Timeout in 5 seconds."))
25 | };
26 | }
27 |
28 | pub fn empty_cache() {
29 | let mut dir = dirs::cache_dir().unwrap();
30 | dir.push("RustPlayer");
31 | if dir.exists() && dir.is_dir() {
32 | let it = read_dir(dir.clone());
33 | if let Ok(dirs) = it {
34 | for dir in dirs {
35 | if let Ok(d) = dir {
36 | std::fs::remove_file(d.path()).unwrap();
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/util/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | pub mod lyrics;
19 | pub mod m3u8;
20 | pub mod net;
21 |
--------------------------------------------------------------------------------
/src/util/net.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{Debug, Display, Formatter};
2 |
3 | use std::sync::mpsc::Sender;
4 |
5 | pub struct DownloadTimeoutError {
6 | msg: String,
7 | }
8 |
9 | impl Display for DownloadTimeoutError {
10 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
11 | write!(f, "Download Timeout! {}", self.msg.as_str())
12 | }
13 | }
14 |
15 | impl Debug for DownloadTimeoutError {
16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
17 | write!(f, "Download Timeout: {}", self.msg.as_str())
18 | }
19 | }
20 |
21 | impl failure::Fail for DownloadTimeoutError {}
22 |
23 | #[tokio::main]
24 | pub async fn download(url: &str, tx: &Sender) -> std::result::Result<(), failure::Error> {
25 | let resp = reqwest::get(url).await?.text().await?;
26 | tx.send(resp)?;
27 | Ok(())
28 | }
29 |
30 | #[tokio::main]
31 | pub async fn download_as_bytes(
32 | url: &str,
33 | tx: &Sender,
34 | ) -> std::result::Result<(), failure::Error> {
35 | let resp = reqwest::get(url).await?.bytes().await?;
36 | tx.send(resp)?;
37 | Ok(())
38 | }
39 |
--------------------------------------------------------------------------------
/tests/lyrics.rs:
--------------------------------------------------------------------------------
1 | // Copyright (C) 2022 KetaNetwork
2 | //
3 | // This file is part of RustPlayer.
4 | //
5 | // RustPlayer is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // RustPlayer is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with RustPlayer. If not, see .
17 |
18 | include!("../src/util/lyrics.rs");
19 |
20 | #[test]
21 | fn test_lyrics() {
22 | let lrc = Lyrics::from_music_path("assets/test.lrc");
23 | assert_ne!(lrc.count(), 0)
24 | }
25 |
--------------------------------------------------------------------------------
/tests/m3u8.rs:
--------------------------------------------------------------------------------
1 | use m3u8_rs::Playlist;
2 | use std::{sync::mpsc, thread, time::Duration};
3 |
4 | include!("../src/util/net.rs");
5 |
6 | #[test]
7 | fn fetch_and_play() {
8 | let src = "http://ngcdn002.cnr.cn/live/jjzs/index.m3u8";
9 | let (tx, rx) = mpsc::channel();
10 | thread::spawn(move || {
11 | download(&src.to_owned(), &tx).unwrap();
12 | });
13 | match rx.recv_timeout(Duration::from_secs(5)) {
14 | Ok(s) => {
15 | println!("{}", s);
16 | let entity = m3u8_rs::parse_playlist(s.as_bytes()).unwrap();
17 | let list = entity.1;
18 | match list {
19 | Playlist::MasterPlaylist(_master_play_list) => {}
20 | Playlist::MediaPlaylist(media_play_list) => {
21 | for seg in &media_play_list.segments {
22 | println!("{}", seg.uri);
23 | }
24 | assert_ne!(media_play_list.segments.len(), 0);
25 | }
26 | }
27 | }
28 | Err(_e) => {
29 | assert!(false);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 | .vscode
5 |
6 | assets/*.raw
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ffmpeg-decoder"
3 | version = "0.1.3"
4 | authors = ["tarkah "]
5 | edition = "2018"
6 | license = "MIT"
7 | description = "Decodes audio files using ffmpeg with rust. Can be used as a rodio source."
8 | documentation = "https://docs.rs/ffmpeg-decoder"
9 | repository = "https://github.com/tarkah/ffmpeg-decoder-rs"
10 | readme = "README.md"
11 | keywords = ["audio", "ffmpeg", "rodio"]
12 | categories = ["multimedia::audio", "multimedia::encoding"]
13 |
14 | [lib]
15 | name = "ffmpeg_decoder"
16 | path = "src/lib.rs"
17 |
18 | [features]
19 | default = []
20 | rodio_source = ['rodio']
21 |
22 | [dependencies]
23 | ffmpeg-sys-next = { git="https://github.com/KetaDotCC/rust-ffmpeg-sys.git", branch="master", default-features=false, features=["avcodec", "avformat", "swresample", "static"] }
24 |
25 | thiserror = "1.0"
26 | log = "0.4"
27 |
28 | rodio = { version = "0.17.0", default-features=false, optional=true }
29 |
30 | [workspace]
31 | members = [
32 | ".",
33 | "cli"
34 | ]
35 | default-members = [
36 | "cli",
37 | ]
38 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 tarkah
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/README.md:
--------------------------------------------------------------------------------
1 | # ffmpeg-decoder
2 | [](https://crates.io/crates/ffmpeg-decoder)
3 | [](https://docs.rs/ffmpeg-decoder)
4 |
5 |
6 | Decodes audio files and converts sample format to signed 16bit. Can
7 | be used as a playback source with [rodio](https://github.com/RustAudio/rodio).
8 |
9 |
10 | ## Rodio Source
11 |
12 | `Decoder` implies rodio's `Source` trait, as well as `Iterator`. Enable feature
13 | flag `rodio_source` to include this. Decoder can then be used as a source for Rodio,
14 | with the benefits of being able to decode everything ffmpeg supports.
15 |
16 |
17 | ## Testing with CLI
18 |
19 |
20 | ### Convert input file to signed 16bit and save as `.raw` alongisde original
21 | ```
22 | cargo run --release -- convert path/to/test.mp3
23 | ```
24 |
25 | ### Play with rodio
26 | ```
27 | cargo run --release -- play path/to/test.flac
28 | ```
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ffmpeg-decoder-cli"
3 | version = "0.1.0"
4 | authors = ["tarkah "]
5 | edition = "2018"
6 |
7 | [dependencies]
8 | ffmpeg-decoder = { path = "../", features = ['rodio_source'] }
9 |
10 | rodio = { version = "0.11", default-features=false }
11 |
12 | anyhow = "1.0"
13 | env_logger = "0.7"
14 | log = "0.4"
15 | structopt = "0.3"
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use env_logger::Env;
3 | use log::{error, info};
4 | use structopt::StructOpt;
5 |
6 | use rodio::Sink;
7 |
8 | use std::path::PathBuf;
9 |
10 | fn main() {
11 | env_logger::from_env(Env::default().default_filter_or("info")).init();
12 |
13 | let opts = Opts::from_args();
14 |
15 | let status = match opts.command {
16 | Command::Convert { input } => decode_to_file(input),
17 | Command::Play { input } => play_file(input),
18 | };
19 |
20 | if let Err(e) = status {
21 | log_error(e);
22 | std::process::exit(1);
23 | }
24 | }
25 |
26 | fn decode_to_file(input: PathBuf) -> Result<(), Error> {
27 | let decoder = ffmpeg_decoder::Decoder::open(&input)?;
28 |
29 | let samples = decoder.collect::>();
30 |
31 | let samples_u8 =
32 | unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 2) };
33 |
34 | let mut out_path: PathBuf = input;
35 | out_path.set_extension("raw");
36 |
37 | std::fs::write(&out_path, samples_u8)?;
38 |
39 | info!(
40 | "File successfully decoded, converted and saved to: {:?}",
41 | out_path
42 | );
43 |
44 | Ok(())
45 | }
46 |
47 | fn play_file(input: PathBuf) -> Result<(), Error> {
48 | let decoder = ffmpeg_decoder::Decoder::open(&input)?;
49 |
50 | let device = rodio::default_output_device().unwrap();
51 | let sink = Sink::new(&device);
52 |
53 | sink.append(decoder);
54 | sink.play();
55 | sink.sleep_until_end();
56 |
57 | Ok(())
58 | }
59 |
60 | fn log_error(e: Error) {
61 | error!("{}", e);
62 | }
63 |
64 | #[derive(StructOpt)]
65 | #[structopt(
66 | name = "libav-decoder-cli",
67 | about = "Convert input audio file sample format to signed 16bit"
68 | )]
69 | struct Opts {
70 | #[structopt(subcommand)]
71 | command: Command,
72 | }
73 |
74 | #[derive(StructOpt)]
75 | enum Command {
76 | /// Convert file and save as `.raw` alongside input file
77 | Convert {
78 | /// Input audio file
79 | #[structopt(parse(from_os_str))]
80 | input: PathBuf,
81 | },
82 | /// Play input file
83 | Play {
84 | /// Input audio file
85 | #[structopt(parse(from_os_str))]
86 | input: PathBuf,
87 | },
88 | }
89 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/src/decoder.rs:
--------------------------------------------------------------------------------
1 | use crate::error::Error;
2 |
3 | use ffmpeg_sys_next::{
4 | self, av_frame_alloc, av_frame_free, av_frame_unref, av_freep, av_get_alt_sample_fmt,
5 | av_get_bytes_per_sample, av_get_channel_layout_nb_channels, av_get_sample_fmt_name,
6 | av_init_packet, av_packet_unref, av_read_frame, av_sample_fmt_is_planar,
7 | av_samples_alloc, av_samples_get_buffer_size, avcodec_alloc_context3, avcodec_close,
8 | avcodec_find_decoder, avcodec_free_context, avcodec_open2, avcodec_parameters_to_context,
9 | avcodec_receive_frame, avcodec_send_packet, avformat_close_input, avformat_find_stream_info,
10 | avformat_open_input, swr_alloc_set_opts, swr_convert, swr_get_out_samples, swr_init, AVCodec,
11 | AVCodecContext, AVFormatContext, AVFrame, AVMediaType, AVPacket, AVSampleFormat, AVStream,
12 | };
13 | use std::ffi::{CStr, CString};
14 | use std::path::Path;
15 | use std::ptr;
16 | use std::slice;
17 | use std::time::Duration;
18 |
19 | use log::{error, info};
20 |
21 | const AVERROR_EOF: i32 = -0x20_464_F45;
22 | const AVERROR_EAGAIN: i32 = -11;
23 | const AVERROR_EDEADLK: i32 = -35;
24 | const DEFAULT_CONVERSION_FORMAT: AVSampleFormat = AVSampleFormat::AV_SAMPLE_FMT_S16;
25 |
26 | pub struct Decoder {
27 | format_ctx: FormatContext,
28 | stream: Stream,
29 | codec_ctx: CodecContext,
30 | frame: Frame,
31 | packet: Packet,
32 | swr_ctx: Option,
33 | current_frame: Vec,
34 | first_frame_stored: bool,
35 | }
36 |
37 | impl Decoder {
38 | pub fn open(path: impl AsRef) -> Result {
39 | // Note: No need to register av for newer ffmpeg (>4).
40 | // unsafe { av_register_all() };
41 |
42 | // Open the file and get the format context
43 | let format_ctx = FormatContext::open(&path.as_ref().display().to_string())?;
44 |
45 | // Find first audio stream in file
46 | format_ctx.find_stream_info()?;
47 | let stream = format_ctx.get_audio_stream()?;
48 |
49 | // Get the streams codec
50 | let codec = stream.get_codec()?;
51 |
52 | // Setup codec context and intialize
53 | let codec_ctx = codec.get_context()?;
54 | codec_ctx.copy_parameters_from_stream(&stream)?;
55 | codec_ctx.request_non_planar_format();
56 | codec_ctx.initialize()?;
57 |
58 | print_codec_info(&codec_ctx);
59 |
60 | // Allocate frame
61 | let frame = Frame::new()?;
62 |
63 | // Initialize packet
64 | let packet = Packet::new();
65 |
66 | // Initialize swr context, if conversion is needed
67 | let swr_ctx = if codec_ctx.sample_format() != DEFAULT_CONVERSION_FORMAT {
68 | Some(SwrContext::new(&codec_ctx)?)
69 | } else {
70 | None
71 | };
72 |
73 | Ok(Decoder {
74 | format_ctx,
75 | stream,
76 | codec_ctx,
77 | frame,
78 | packet,
79 | swr_ctx,
80 | current_frame: vec![],
81 | first_frame_stored: false,
82 | })
83 | }
84 |
85 | fn read_next_frame(&mut self) -> ReadFrameStatus {
86 | let status =
87 | unsafe { av_read_frame(self.format_ctx.inner, self.packet.inner.as_mut_ptr()) };
88 |
89 | match status {
90 | AVERROR_EOF => ReadFrameStatus::Eof,
91 | _ if status != 0 => ReadFrameStatus::Other(status),
92 | _ => ReadFrameStatus::Ok,
93 | }
94 | }
95 |
96 | fn send_packet_for_decoding(&mut self) -> SendPacketStatus {
97 | let status =
98 | unsafe { avcodec_send_packet(self.codec_ctx.inner, self.packet.inner.as_mut_ptr()) };
99 |
100 | match status {
101 | 0 => SendPacketStatus::Ok,
102 | _ => SendPacketStatus::Other(status),
103 | }
104 | }
105 |
106 | fn receive_decoded_frame(&self) -> ReceiveFrameStatus {
107 | let status = unsafe { avcodec_receive_frame(self.codec_ctx.inner, self.frame.inner) };
108 |
109 | match status {
110 | 0 => ReceiveFrameStatus::Ok,
111 | AVERROR_EAGAIN => ReceiveFrameStatus::Again,
112 | AVERROR_EDEADLK => ReceiveFrameStatus::Deadlk,
113 | _ => ReceiveFrameStatus::Other(status),
114 | }
115 | }
116 |
117 | fn convert_and_store_frame(&mut self) {
118 | let num_samples = self.frame.num_samples();
119 | let channel_layout = self.frame.channel_layout();
120 | let num_channels = unsafe { av_get_channel_layout_nb_channels(channel_layout) };
121 |
122 | let extended_data = self.frame.extended_data();
123 |
124 | let mut out_buf = std::ptr::null_mut::();
125 |
126 | let out_slice = if self.swr_ctx.is_some() {
127 | let out_samples =
128 | unsafe { swr_get_out_samples(self.swr_ctx.as_ref().unwrap().inner, num_samples) };
129 |
130 | unsafe {
131 | av_samples_alloc(
132 | &mut out_buf,
133 | ptr::null_mut(),
134 | num_channels,
135 | out_samples,
136 | DEFAULT_CONVERSION_FORMAT,
137 | 0,
138 | )
139 | };
140 |
141 | unsafe {
142 | swr_convert(
143 | self.swr_ctx.as_ref().unwrap().inner,
144 | &mut out_buf,
145 | out_samples,
146 | extended_data,
147 | num_samples,
148 | )
149 | };
150 |
151 | let out_size = unsafe {
152 | av_samples_get_buffer_size(
153 | ptr::null_mut(),
154 | num_channels,
155 | out_samples,
156 | DEFAULT_CONVERSION_FORMAT,
157 | 0,
158 | )
159 | };
160 |
161 | unsafe { slice::from_raw_parts(out_buf, out_size as usize) }
162 | } else {
163 | unsafe {
164 | slice::from_raw_parts(
165 | extended_data.as_ref().unwrap().as_ref().unwrap(),
166 | self.frame.inner.as_ref().unwrap().linesize[0] as usize,
167 | )
168 | }
169 | };
170 |
171 | if !self.current_frame.is_empty() {
172 | self.current_frame.drain(..);
173 | }
174 |
175 | self.current_frame.extend_from_slice(out_slice);
176 |
177 | if self.swr_ctx.is_some() {
178 | // Free samples buffer
179 | unsafe { av_freep(&mut out_buf as *mut _ as _) };
180 | }
181 |
182 | unsafe { av_frame_unref(self.frame.inner) };
183 | }
184 |
185 | fn frame_for_stream(&self) -> bool {
186 | unsafe { self.packet.inner.as_ptr().as_ref().unwrap().stream_index == self.stream.index }
187 | }
188 |
189 | fn reset_packet(&mut self) {
190 | unsafe { av_packet_unref(self.packet.inner.as_mut_ptr()) };
191 | }
192 |
193 | fn next_sample(&mut self) -> i16 {
194 | let sample_u8: [u8; 2] = [self.current_frame.remove(0), self.current_frame.remove(0)];
195 |
196 | ((sample_u8[1] as i16) << 8) | sample_u8[0] as i16
197 | }
198 |
199 | fn process_next_frame(&mut self) -> Option> {
200 | match self.read_next_frame() {
201 | ReadFrameStatus::Ok => {}
202 | ReadFrameStatus::Eof => {
203 | return None;
204 | }
205 | ReadFrameStatus::Other(status) => {
206 | error!("{}", Error::ReadFrame(status));
207 | return None;
208 | }
209 | }
210 |
211 | if !self.frame_for_stream() {
212 | self.reset_packet();
213 | return self.process_next_frame();
214 | }
215 |
216 | match self.send_packet_for_decoding() {
217 | SendPacketStatus::Ok => self.reset_packet(),
218 | SendPacketStatus::Other(status) => {
219 | error!("{}", Error::SendPacket(status));
220 | return None;
221 | }
222 | }
223 |
224 | match self.receive_decoded_frame() {
225 | ReceiveFrameStatus::Ok => {}
226 | ReceiveFrameStatus::Again | ReceiveFrameStatus::Deadlk => {
227 | return self.process_next_frame()
228 | }
229 | ReceiveFrameStatus::Other(status) => {
230 | error!("{}", Error::ReceiveFrame(status));
231 | return None;
232 | }
233 | }
234 |
235 | self.convert_and_store_frame();
236 |
237 | Some(Ok(()))
238 | }
239 |
240 | fn cleanup(&mut self) {
241 | // Drain the decoder.
242 | drain_decoder(self.codec_ctx.inner).unwrap();
243 |
244 | unsafe {
245 | // Free all data used by the frame.
246 | av_frame_free(&mut self.frame.inner);
247 |
248 | // Close the context and free all data associated to it, but not the context itself.
249 | avcodec_close(self.codec_ctx.inner);
250 |
251 | // Free the context itself.
252 | avcodec_free_context(&mut self.codec_ctx.inner);
253 |
254 | // Close the input.
255 | avformat_close_input(&mut self.format_ctx.inner);
256 | }
257 | }
258 |
259 | pub(crate) fn _current_frame_len(&self) -> Option {
260 | Some(self.current_frame.len())
261 | }
262 |
263 | pub(crate) fn _channels(&self) -> u16 {
264 | self.codec_ctx.channels() as _
265 | }
266 |
267 | pub(crate) fn _sample_rate(&self) -> u32 {
268 | self.codec_ctx.sample_rate() as _
269 | }
270 |
271 | pub(crate) fn _total_duration(&self) -> Option {
272 | //TODO let duration = self.stream.duration();
273 | None
274 | }
275 | }
276 |
277 | unsafe impl Send for Decoder {}
278 |
279 | impl Iterator for Decoder {
280 | type Item = i16;
281 |
282 | #[inline]
283 | fn next(&mut self) -> Option {
284 | if !self.first_frame_stored {
285 | if self.process_next_frame().is_none() {
286 | self.cleanup();
287 | return None;
288 | }
289 |
290 | self.first_frame_stored = true;
291 |
292 | return Some(self.next_sample());
293 | }
294 |
295 | if !self.current_frame.is_empty() {
296 | return Some(self.next_sample());
297 | }
298 |
299 | match self.receive_decoded_frame() {
300 | ReceiveFrameStatus::Ok => {
301 | self.convert_and_store_frame();
302 | Some(self.next_sample())
303 | }
304 | ReceiveFrameStatus::Again | ReceiveFrameStatus::Deadlk => {
305 | if self.process_next_frame().is_none() {
306 | self.cleanup();
307 | return None;
308 | }
309 |
310 | Some(self.next_sample())
311 | }
312 | ReceiveFrameStatus::Other(status) => {
313 | error!("{}", Error::ReceiveFrame(status));
314 | self.cleanup();
315 | None
316 | }
317 | }
318 | }
319 | }
320 |
321 | struct FormatContext {
322 | inner: *mut AVFormatContext,
323 | }
324 |
325 | impl FormatContext {
326 | fn open(path: &str) -> Result {
327 | let mut inner = std::ptr::null_mut::();
328 |
329 | let path = CString::new(path).unwrap();
330 |
331 | let status = unsafe {
332 | avformat_open_input(
333 | &mut inner,
334 | path.as_ptr(),
335 | std::ptr::null_mut(),
336 | std::ptr::null_mut(),
337 | )
338 | };
339 | if status != 0 {
340 | return Err(Error::InitializeFormatContext);
341 | }
342 |
343 | Ok(FormatContext { inner })
344 | }
345 |
346 | /// Look at first few frames to determine stream info
347 | fn find_stream_info(&self) -> Result<(), Error> {
348 | let status = unsafe { avformat_find_stream_info(self.inner, ptr::null_mut()) };
349 | if status < 0 {
350 | return Err(Error::FindStreamInfo);
351 | }
352 | Ok(())
353 | }
354 |
355 | /// Get the first audio stream
356 | fn get_audio_stream(&self) -> Result {
357 | let num_streams = unsafe { self.inner.as_ref().unwrap().nb_streams };
358 | let streams = unsafe { self.inner.as_ref().unwrap().streams };
359 |
360 | let streams = unsafe { slice::from_raw_parts(streams, num_streams as usize) };
361 |
362 | let stream_idx = find_audio_stream(streams)?;
363 |
364 | Ok(Stream::new(streams[0], stream_idx))
365 | }
366 | }
367 |
368 | struct SwrContext {
369 | inner: *mut ffmpeg_sys_next::SwrContext,
370 | }
371 |
372 | impl SwrContext {
373 | fn new(codec_ctx: &CodecContext) -> Result {
374 | let swr_ctx: *mut ffmpeg_sys_next::SwrContext = unsafe {
375 | swr_alloc_set_opts(
376 | ptr::null_mut(),
377 | codec_ctx.channel_layout() as i64,
378 | DEFAULT_CONVERSION_FORMAT,
379 | codec_ctx.sample_rate(),
380 | codec_ctx.channel_layout() as i64,
381 | codec_ctx.sample_format(),
382 | codec_ctx.sample_rate(),
383 | 0,
384 | ptr::null_mut(),
385 | )
386 | };
387 |
388 | let status = unsafe { swr_init(swr_ctx) };
389 | if status != 0 {
390 | return Err(Error::InitializeSwr);
391 | }
392 |
393 | Ok(SwrContext { inner: swr_ctx })
394 | }
395 | }
396 |
397 | struct Packet {
398 | inner: std::mem::MaybeUninit,
399 | }
400 |
401 | impl Packet {
402 | fn new() -> Packet {
403 | let mut packet = std::mem::MaybeUninit::uninit();
404 |
405 | unsafe { av_init_packet(packet.as_mut_ptr()) };
406 |
407 | Packet { inner: packet }
408 | }
409 | }
410 |
411 | struct Frame {
412 | inner: *mut AVFrame,
413 | }
414 |
415 | impl Frame {
416 | fn new() -> Result {
417 | let frame: *mut AVFrame = unsafe { av_frame_alloc() };
418 |
419 | if frame.is_null() {
420 | return Err(Error::NullFrame);
421 | }
422 |
423 | Ok(Frame { inner: frame })
424 | }
425 |
426 | fn num_samples(&self) -> i32 {
427 | unsafe { self.inner.as_ref().unwrap().nb_samples }
428 | }
429 |
430 | fn channel_layout(&self) -> u64 {
431 | unsafe { self.inner.as_ref().unwrap().channel_layout }
432 | }
433 |
434 | fn extended_data(&self) -> *mut *const u8 {
435 | unsafe { self.inner.as_ref().unwrap().extended_data as *mut *const u8 }
436 | }
437 | }
438 |
439 | struct Stream {
440 | inner: *mut AVStream,
441 | index: i32,
442 | }
443 |
444 | impl Stream {
445 | fn new(inner: *mut AVStream, index: i32) -> Stream {
446 | Stream { inner, index }
447 | }
448 |
449 | fn get_codec(&self) -> Result {
450 | // Get streams codec
451 | let codec_params = unsafe { self.inner.as_ref().unwrap().codecpar };
452 | let codec_id = unsafe { codec_params.as_ref().unwrap().codec_id };
453 |
454 | let codec: *mut AVCodec = unsafe { avcodec_find_decoder(codec_id) as _ };
455 | if codec.is_null() {
456 | return Err(Error::NullCodec);
457 | }
458 |
459 | Ok(Codec::new(codec))
460 | }
461 |
462 | #[allow(dead_code)]
463 | fn duration(&self) -> i64 {
464 | unsafe { self.inner.as_ref().unwrap().duration }
465 | }
466 | }
467 |
468 | struct CodecContext {
469 | inner: *mut AVCodecContext,
470 | codec: *mut AVCodec,
471 | }
472 |
473 | impl CodecContext {
474 | fn new(inner: *mut AVCodecContext, codec: *mut AVCodec) -> CodecContext {
475 | CodecContext { inner, codec }
476 | }
477 |
478 | fn copy_parameters_from_stream(&self, stream: &Stream) -> Result<(), Error> {
479 | let params = unsafe { stream.inner.as_ref().unwrap().codecpar };
480 |
481 | let status = unsafe { avcodec_parameters_to_context(self.inner, params) };
482 |
483 | if status != 0 {
484 | return Err(Error::CodecParamsToContext);
485 | }
486 |
487 | Ok(())
488 | }
489 |
490 | fn request_non_planar_format(&self) {
491 | unsafe {
492 | let sample_fmt = self.inner.as_ref().unwrap().sample_fmt;
493 | let alt_format = av_get_alt_sample_fmt(sample_fmt, 0);
494 |
495 | self.inner.as_mut().unwrap().request_sample_fmt = alt_format;
496 | }
497 | }
498 |
499 | fn initialize(&self) -> Result<(), Error> {
500 | let status = unsafe { avcodec_open2(self.inner, self.codec, &mut std::ptr::null_mut()) };
501 |
502 | if status != 0 {
503 | return Err(Error::InitializeDecoder);
504 | }
505 |
506 | Ok(())
507 | }
508 |
509 | fn codec_name(&self) -> &str {
510 | let name = unsafe { CStr::from_ptr(self.codec.as_ref().unwrap().long_name) };
511 |
512 | name.to_str().unwrap()
513 | }
514 |
515 | fn sample_format(&self) -> AVSampleFormat {
516 | unsafe { self.inner.as_ref().unwrap().sample_fmt }
517 | }
518 |
519 | fn sample_format_name(&self) -> &str {
520 | let sample_fmt = unsafe { CStr::from_ptr(av_get_sample_fmt_name(self.sample_format())) };
521 |
522 | sample_fmt.to_str().unwrap()
523 | }
524 |
525 | fn sample_rate(&self) -> i32 {
526 | unsafe { self.inner.as_ref().unwrap().sample_rate }
527 | }
528 |
529 | fn sample_size(&self) -> i32 {
530 | unsafe { av_get_bytes_per_sample(self.inner.as_ref().unwrap().sample_fmt) }
531 | }
532 |
533 | fn channels(&self) -> i32 {
534 | unsafe { self.inner.as_ref().unwrap().channels }
535 | }
536 |
537 | fn channel_layout(&self) -> u64 {
538 | unsafe { self.inner.as_ref().unwrap().channel_layout }
539 | }
540 |
541 | fn is_planar(&self) -> i32 {
542 | unsafe { av_sample_fmt_is_planar(self.inner.as_ref().unwrap().sample_fmt) }
543 | }
544 | }
545 |
546 | struct Codec {
547 | inner: *mut AVCodec,
548 | }
549 |
550 | impl Codec {
551 | fn new(inner: *mut AVCodec) -> Codec {
552 | Codec { inner }
553 | }
554 |
555 | fn get_context(&self) -> Result {
556 | let ctx: *mut AVCodecContext = unsafe { avcodec_alloc_context3(self.inner) };
557 |
558 | if ctx.is_null() {
559 | return Err(Error::NullCodecContext);
560 | }
561 |
562 | Ok(CodecContext::new(ctx, self.inner))
563 | }
564 | }
565 |
566 | enum ReadFrameStatus {
567 | Ok,
568 | Eof,
569 | Other(i32),
570 | }
571 |
572 | enum SendPacketStatus {
573 | Ok,
574 | Other(i32),
575 | }
576 |
577 | enum ReceiveFrameStatus {
578 | Ok,
579 | Again,
580 | Deadlk,
581 | Other(i32),
582 | }
583 |
584 | fn find_audio_stream(streams: &[*mut AVStream]) -> Result {
585 | for stream in streams {
586 | let codec_type = unsafe {
587 | stream
588 | .as_ref()
589 | .unwrap()
590 | .codecpar
591 | .as_ref()
592 | .unwrap()
593 | .codec_type
594 | };
595 | let index = unsafe { stream.as_ref().unwrap().index };
596 |
597 | if codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO {
598 | return Ok(index);
599 | }
600 | }
601 |
602 | Err(Error::NoAudioStream)
603 | }
604 |
605 | fn print_codec_info(codec_ctx: &CodecContext) {
606 | info!("Codec: {}", codec_ctx.codec_name());
607 | info!("Sample Format: {}", codec_ctx.sample_format_name());
608 | info!("Sample Rate: {}", codec_ctx.sample_rate());
609 | info!("Sample Size: {}", codec_ctx.sample_size());
610 | info!("Channels: {}", codec_ctx.channels());
611 | info!("Planar: {}", codec_ctx.is_planar());
612 | }
613 |
614 | fn drain_decoder(codec_ctx: *mut AVCodecContext) -> Result<(), Error> {
615 | let status = unsafe { avcodec_send_packet(codec_ctx, std::ptr::null()) };
616 | if status == 0 {
617 | } else {
618 | return Err(Error::DrainDecoder(status));
619 | }
620 |
621 | Ok(())
622 | }
623 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/src/error.rs:
--------------------------------------------------------------------------------
1 | use thiserror::Error;
2 |
3 | #[derive(Error, Debug)]
4 | pub enum Error {
5 | #[error("Failed to initialize format context")]
6 | InitializeFormatContext,
7 | #[error("Could not find stream in file")]
8 | FindStreamInfo,
9 | #[error("Could not find any audio stream")]
10 | NoAudioStream,
11 | #[error("Null codec pointer")]
12 | NullCodec,
13 | #[error("Null codec context pointer")]
14 | NullCodecContext,
15 | #[error("Copying params to codec context")]
16 | CodecParamsToContext,
17 | #[error("Failed to initialize decoder")]
18 | InitializeDecoder,
19 | #[error("Null frame pointer")]
20 | NullFrame,
21 | #[error("Error reading frame: {0}")]
22 | ReadFrame(i32),
23 | #[error("Error sending packet: {0}")]
24 | SendPacket(i32),
25 | #[error("Error draining decoder: {0}")]
26 | DrainDecoder(i32),
27 | #[error("Error receiving frame: {0}")]
28 | ReceiveFrame(i32),
29 | #[error("Failed to initialize swr context")]
30 | InitializeSwr,
31 | }
32 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Decodes audio files using ffmpeg bindings
2 | //!
3 | //! Create a [`Decoder`](struct.Decoder.html) by supplying a `Path` to an audio file. [`Decoder`](struct.Decoder.html)
4 | //! implies `Iterator` where each iteration returns a single `i16` signed 16bit sample.
5 | //! Also implements [rodio's](https://github.com/RustAudio/rodio) [`Source`](https://docs.rs/rodio/latest/rodio/source/trait.Source.html) trait, where
6 | //! the [`Decoder`](struct.Decoder.html) can be supplied as a sink source for playback.
7 | //!
8 | //! ### Features Flags
9 | //!
10 | //! - `rodio_source` to enable rodio's [`Source`](https://docs.rs/rodio/latest/rodio/source/trait.Source.html) trait
11 | //!
12 | //!
13 | //! ## Example as Rodio Source
14 | //!
15 | //! ```rust
16 | //! use rodio::Sink;
17 | //! use std::path::PathBuf;
18 | //!
19 | //! fn play_file(input: PathBuf) -> Result<(), Error> {
20 | //! let decoder = ffmpeg_decoder::Decoder::open(&input)?;
21 | //!
22 | //! let device = rodio::default_output_device().unwrap();
23 | //! let sink = Sink::new(&device);
24 | //!
25 | //! sink.append(decoder);
26 | //! sink.play();
27 | //! sink.sleep_until_end();
28 | //!
29 | //! Ok(())
30 | //! }
31 | //! ```
32 | mod decoder;
33 | pub use decoder::Decoder;
34 |
35 | mod error;
36 | pub use error::Error;
37 |
38 | #[cfg(feature = "rodio_source")]
39 | mod rodio;
40 |
--------------------------------------------------------------------------------
/thirdparty/ffmpeg-decoder-rs/src/rodio.rs:
--------------------------------------------------------------------------------
1 | use crate::Decoder;
2 |
3 | use rodio::source::Source;
4 |
5 | use std::time::Duration;
6 |
7 | impl Source for Decoder {
8 | #[inline]
9 | fn current_frame_len(&self) -> Option {
10 | self._current_frame_len()
11 | }
12 |
13 | #[inline]
14 | fn channels(&self) -> u16 {
15 | self._channels()
16 | }
17 |
18 | #[inline]
19 | fn sample_rate(&self) -> u32 {
20 | self._sample_rate()
21 | }
22 |
23 | #[inline]
24 | fn total_duration(&self) -> Option {
25 | self._total_duration()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------