Repository: owncloud/ocis
Pull request #7239 (2023-09-06 12:50:18)

Initial implementation of /me/drives/sharedByMe endpoint
This adds a (still incomplete) implementation of the new /me/drives/sharedByMe endpoint. 

Still missing / clarifications needed:
- [ ] Populating the 'roles' property of the permissions
- [ ] Clarify 'roles' vs. 'types'. https://github.com/owncloud/libre-graph-api/pull/112 isn't exactly clear about this. The examples contains 'roles' for links instead or 'type' as defined for MS graph. 
- [x] Populating the 'displayName' property of the users/groups in the permission.
- [ ] Are reshares supposed to be exposed for the original share creator? Probably not. According to https://github.com/owncloud/libre-graph-api/pull/112 `/me/drive/sharedByMe` only includes shares created by the current user.
- [ ] Optimization: Add concurrency to the Stat() calls and User lookups.
- [ ] more ...

IMO the open items should be tackled in follow-up pull-request. To keep this PR from growing to big.

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


rhafer: converting to draft, as we need to finish the spec PR first.
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [2 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![77.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '77.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [77.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [2 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![72.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '72.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [72.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![69.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '69.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [69.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![6.9%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '6.9%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [6.9% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![67.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '67.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [67.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![5.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '5.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [5.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![67.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '67.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [67.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![5.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '5.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [5.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![67.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '67.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [67.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![5.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '5.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [5.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![69.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '69.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [69.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![5.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '5.2%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [5.2% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![62.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '62.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [62.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![4.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/5-16px.png '4.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [4.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![82.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '82.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [82.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![4.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/5-16px.png '4.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [4.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![3.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/5-16px.png '3.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [3.5% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7239)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7239&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7239&resolved=false&types=CODE_SMELL)

[![79.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '79.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list) [79.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_coverage&view=list)  
[![3.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/5-16px.png '3.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list) [3.5% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7239&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index e3de65b7777..f4373366c1b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -8,13 +8,9 @@ assignees: ''
 ---
 
 ## Describe the bug
-
 A clear and concise description of what the bug is.
 
 ## Steps to reproduce
-
-Steps to reproduce the behavior:
-
 1.
 2.
 3.
@@ -26,7 +22,6 @@ A clear and concise description of what you expected to happen.
 A clear and concise description of what happened.
 
 ## Setup
-
 Please describe how you started the server and provide a list of relevant environment variables or configuration files.
 
 <details>
diff --git a/.github/ISSUE_TEMPLATE/user-story.md b/.github/ISSUE_TEMPLATE/user-story.md
new file mode 100644
index 00000000000..9c9ce4901a8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/user-story.md
@@ -0,0 +1,39 @@
+---
+name: User story
+about: Suggest an idea the agile way :-)
+title: ''
+labels: Type:Story
+assignees: ''
+
+---
+
+# Description
+
+## User Stories
+
+* > As a ..., I want to ... so that ... (please stick to who, what, why)
+
+## Value
+
+## Acceptance Criteria
+
+## Definition of ready
+- [ ] Everybody needs to understand the value written in the user story
+- [ ] Acceptance criteria have to be defined
+- [ ] All dependencies of the user story need to be identified
+- [ ] Feature should be seen from an end user perspective
+- [ ] Story has to be estimated
+- [ ] Story points need to be less than 20
+
+## Definition of done
+- Functional requirements
+  - [ ] Functionality described in the user story works
+  - [ ] Acceptance criteria are fulfilled
+- Quality
+  - [ ] Code review happened
+  - [ ] CI is green (that includes new and existing automated tests)
+  - [ ] Critical code received unit tests by the developer
+- Non-functional requirements
+  - [ ] No sonar cloud issues
+- Configuration changes
+  - [ ] The next branch of the ocis charts is compatible
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 610527e807d..e8117e4a6a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ The following sections list the changes for unreleased.
 ## Summary
 
 * Enhancement - Bump reva: [#7629](https://github.com/owncloud/ocis/pull/7629)
+* Enhancement - Add support for audio files to the thumbnails service: [#7491](https://github.com/owncloud/ocis/pull/7491)
+* Enhancement - Store and index metadata: [#7490](https://github.com/owncloud/ocis/pull/7490)
 
 ## Details
 
@@ -15,6 +17,20 @@ The following sections list the changes for unreleased.
    Bumps reva version
 
    https://github.com/owncloud/ocis/pull/7629
+
+* Enhancement - Add support for audio files to the thumbnails service: [#7491](https://github.com/owncloud/ocis/pull/7491)
+
+   The thumbnails service can now extract artwork from audio files (mp3, ogg, flac) and render it
+   just like any other image.
+
+   https://github.com/owncloud/ocis/pull/7491
+
+* Enhancement - Store and index metadata: [#7490](https://github.com/owncloud/ocis/pull/7490)
+
+   Audio metadata is now extracted and stored by the search service. It is available for
+   driveItems in a folder listing using the Graph API.
+
+   https://github.com/owncloud/ocis/pull/7490
 # Changelog for [5.0.0] (2023-10-30)
 
 The following sections list the changes for 5.0.0.
diff --git a/changelog/unreleased/enhancement-add-support-for-audio-files-to-thumbnails-service.md b/changelog/unreleased/enhancement-add-support-for-audio-files-to-thumbnails-service.md
new file mode 100644
index 00000000000..47d67e2849c
--- /dev/null
+++ b/changelog/unreleased/enhancement-add-support-for-audio-files-to-thumbnails-service.md
@@ -0,0 +1,5 @@
+Enhancement: Add support for audio files to the thumbnails service
+
+The thumbnails service can now extract artwork from audio files (mp3, ogg, flac) and render it just like any other image.
+
+https://github.com/owncloud/ocis/pull/7491
diff --git a/changelog/unreleased/enhancement-store-and-index-audio-metadata.md b/changelog/unreleased/enhancement-store-and-index-audio-metadata.md
new file mode 100644
index 00000000000..372f3a57082
--- /dev/null
+++ b/changelog/unreleased/enhancement-store-and-index-audio-metadata.md
@@ -0,0 +1,6 @@
+Enhancement: Store and index metadata
+
+Audio metadata is now extracted and stored by the search service.
+It is available for driveItems in a folder listing using the Graph API.
+
+https://github.com/owncloud/ocis/pull/7490
diff --git a/docs/services/ocm/configuration.md b/docs/services/ocm/configuration.md
new file mode 100644
index 00000000000..eab0de7824d
--- /dev/null
+++ b/docs/services/ocm/configuration.md
@@ -0,0 +1,15 @@
+---
+title: Service Configuration
+date: 2018-05-02T00:00:00+00:00
+weight: 20
+geekdocRepo: https://github.com/owncloud/ocis
+geekdocEditPath: edit/master/docs/services/ocm
+geekdocFilePath: configuration.md
+geekdocCollapseSection: true
+---
+
+## Example YAML Config
+
+{{< include file="services/_includes/ocm-config-example.yaml"  language="yaml" >}}
+
+{{< include file="services/_includes/ocm.md" >}}
diff --git a/docs/services/ocm/create_share_flow.md b/docs/services/ocm/create_share_flow.md
new file mode 100644
index 00000000000..6474ab0e5d8
--- /dev/null
+++ b/docs/services/ocm/create_share_flow.md
@@ -0,0 +1,65 @@
+---
+title: Create Share Flow
+date: 2018-05-02T00:00:00+00:00
+weight: 40
+geekdocRepo: https://github.com/owncloud/ocis
+geekdocEditPath: edit/master/docs/services/ocm
+geekdocFilePath: create_share_flow.md
+geekdocCollapseSection: true
+---
+
+## OCM Create Share Flow
+
+{{< mermaid class="text-center">}}
+sequenceDiagram
+    box Instance A
+        participant osp as ocmsharesprovider
+        participant gwa as Gateway A
+        participant httpa as ocs
+    end
+    actor usera as User A
+    box Instance B
+        participant httpb as ocmd
+        participant gwb as Gateway B
+        participant ocmc as OCMCore
+    end
+
+    Note over usera: A shares a resource with B
+    usera->>+httpa: CreateShare
+        httpa->>+gwa: GetInfoByDomain
+        Note left of gwa: GetInfoByDomain<br/>(ocmproviderauthorizer)
+        gwa-->>-httpa: return
+
+        httpa->>+gwa: GetAcceptedUser
+        Note left of gwa: GetAcceptedUser<br/>(ocminvitemanager)
+        gwa-->>-httpa: return
+
+        httpa->>+gwa: CreateOCMShare
+            gwa->>+osp: CreateOCMShare
+                osp->>+gwa: Stat
+                gwa-->>-osp: return
+
+                Note left of osp: store share in repo
+                
+                osp->>+httpb: POST /shares
+                    httpb->>+gwb: IsProviderAllowed
+                    Note right of gwb: IsProviderAllowed<br/>(ocmproviderauthorizer)
+                    gwb-->>-httpb: return
+
+                    httpb->>+gwb: GetUser
+                    Note right of gwb: GetUser<br/>(userprovider)
+                    gwb-->>-httpb: return
+
+                    httpb->>+gwb: CreateOCMCoreShare
+                        gwb->>+ocmc: CreateOCMCoreShare
+                        Note right of ocmc: StoreReceivedShare
+                        ocmc-->>-gwb: return
+                    gwb-->>-httpb: return
+                httpb-->>-osp: return
+            osp-->>-gwa: return
+        gwa-->>-httpa: return
+        httpa->>+gwa: Stat
+            Note left of gwa: Stat<br/>(storageprovider)
+        gwa-->>-httpa: return
+    httpa-->>-usera: return
+{{< /mermaid >}}
\ No newline at end of file
diff --git a/docs/services/ocm/invitation_flow.md b/docs/services/ocm/invitation_flow.md
new file mode 100644
index 00000000000..58744c2c5e1
--- /dev/null
+++ b/docs/services/ocm/invitation_flow.md
@@ -0,0 +1,55 @@
+---
+title: Invitation flow
+date: 2018-05-02T00:00:00+00:00
+weight: 30
+geekdocRepo: https://github.com/owncloud/ocis
+geekdocEditPath: edit/master/docs/services/ocm
+geekdocFilePath: invitation_flow.md
+geekdocCollapseSection: true
+---
+
+## OCM Invitation Flow
+
+{{< mermaid class="text-center">}}
+sequenceDiagram
+    box Instance A
+        participant ima as InviteManager A
+        participant gwa as Gateway A
+        participant httpa as HTTP Api A (ocm, sm)
+    end
+    actor usera as User A
+    actor userb as User B
+    box Instance B
+        participant httpb as HTTP Api B (ocm, sm)
+        participant gwb as Gateway B
+        participant imb as InviteManager B
+    end
+
+    Note over usera: A creates invitation token
+    usera->>+httpa: POST /generate-invite (sciencemesh)
+        httpa->>+gwa: GenerateInviteToken
+            gwa->>+ima: GenerateInviteToken
+                Note left of ima: store token in repo
+            ima-->>-gwa: return token
+        gwa-->>-httpa: return token
+    httpa-->>-usera: return token
+    
+    Note over usera,userb: A passes token to B
+
+    Note over userb: B accepts invitation
+    userb->>+httpb: POST /accept-invite (sciencemesh)
+        httpb->>+gwb: ForwardInvite
+            gwb->>+imb: ForwardInvite
+                imb->>+httpa: POST /invite-accepted (ocm)
+                    httpa->>+gwa: AcceptInvite
+                        gwa->>+ima: AcceptInvite
+                             Note left of ima: get token from repo
+                             Note left of ima: add remote user
+                        ima-->>-gwa: return
+                    gwa-->>-httpa: return remote user
+                httpa->>-imb: return remote user
+                Note right of imb: add remote user
+            imb-->>-gwb: return
+        gwb-->>-httpb: return
+    httpb-->>-userb: return
+{{< /mermaid >}}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index ca05b2c561f..4c6aa7d6324 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/disintegration/imaging v1.6.2
 	github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
 	github.com/egirna/icap-client v0.1.1
-	github.com/gabriel-vasile/mimetype v1.4.2
+	github.com/gabriel-vasile/mimetype v1.4.3
 	github.com/ggwhite/go-masker v1.0.9
 	github.com/go-chi/chi/v5 v5.0.10
 	github.com/go-chi/cors v1.2.1
@@ -167,6 +167,7 @@ require (
 	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 // indirect
 	github.com/dlclark/regexp2 v1.4.0 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 8bfbefde6af..2c7f4e49b5d 100644
--- a/go.sum
+++ b/go.sum
@@ -1035,6 +1035,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
 github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 h1:simG0vMYFvNriGhaaat7QVVkaVkXzvqcohaBoLZl9Hg=
+github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
@@ -1104,8 +1106,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U=
 github.com/gdexlab/go-render v1.0.1/go.mod h1:wRi5nW2qfjiGj4mPukH4UV0IknS1cHD4VgFTmJX5JzM=
 github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.go b/protogen/gen/ocis/messages/search/v0/search.pb.go
index e9eabf8a727..267fcf0e65f 100644
--- a/protogen/gen/ocis/messages/search/v0/search.pb.go
+++ b/protogen/gen/ocis/messages/search/v0/search.pb.go
@@ -139,6 +139,173 @@ func (x *Reference) GetPath() string {
 	return ""
 }
 
+type Audio struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Album             *string `protobuf:"bytes,1,opt,name=album,proto3,oneof" json:"album,omitempty"`
+	AlbumArtist       *string `protobuf:"bytes,2,opt,name=albumArtist,proto3,oneof" json:"albumArtist,omitempty"`
+	Artist            *string `protobuf:"bytes,3,opt,name=artist,proto3,oneof" json:"artist,omitempty"`
+	Bitrate           *int64  `protobuf:"varint,4,opt,name=bitrate,proto3,oneof" json:"bitrate,omitempty"`
+	Composers         *string `protobuf:"bytes,5,opt,name=composers,proto3,oneof" json:"composers,omitempty"`
+	Copyright         *string `protobuf:"bytes,6,opt,name=copyright,proto3,oneof" json:"copyright,omitempty"`
+	Disc              *int32  `protobuf:"varint,7,opt,name=disc,proto3,oneof" json:"disc,omitempty"`
+	DiscCount         *int32  `protobuf:"varint,8,opt,name=discCount,proto3,oneof" json:"discCount,omitempty"`
+	Duration          *int64  `protobuf:"varint,9,opt,name=duration,proto3,oneof" json:"duration,omitempty"`
+	Genre             *string `protobuf:"bytes,10,opt,name=genre,proto3,oneof" json:"genre,omitempty"`
+	HasDrm            *bool   `protobuf:"varint,11,opt,name=hasDrm,proto3,oneof" json:"hasDrm,omitempty"`
+	IsVariableBitrate *bool   `protobuf:"varint,12,opt,name=isVariableBitrate,proto3,oneof" json:"isVariableBitrate,omitempty"`
+	Title             *string `protobuf:"bytes,13,opt,name=title,proto3,oneof" json:"title,omitempty"`
+	Track             *int32  `protobuf:"varint,14,opt,name=track,proto3,oneof" json:"track,omitempty"`
+	TrackCount        *int32  `protobuf:"varint,15,opt,name=trackCount,proto3,oneof" json:"trackCount,omitempty"`
+	Year              *int32  `protobuf:"varint,16,opt,name=year,proto3,oneof" json:"year,omitempty"`
+}
+
+func (x *Audio) Reset() {
+	*x = Audio{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Audio) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Audio) ProtoMessage() {}
+
+func (x *Audio) ProtoReflect() protoreflect.Message {
+	mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Audio.ProtoReflect.Descriptor instead.
+func (*Audio) Descriptor() ([]byte, []int) {
+	return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *Audio) GetAlbum() string {
+	if x != nil && x.Album != nil {
+		return *x.Album
+	}
+	return ""
+}
+
+func (x *Audio) GetAlbumArtist() string {
+	if x != nil && x.AlbumArtist != nil {
+		return *x.AlbumArtist
+	}
+	return ""
+}
+
+func (x *Audio) GetArtist() string {
+	if x != nil && x.Artist != nil {
+		return *x.Artist
+	}
+	return ""
+}
+
+func (x *Audio) GetBitrate() int64 {
+	if x != nil && x.Bitrate != nil {
+		return *x.Bitrate
+	}
+	return 0
+}
+
+func (x *Audio) GetComposers() string {
+	if x != nil && x.Composers != nil {
+		return *x.Composers
+	}
+	return ""
+}
+
+func (x *Audio) GetCopyright() string {
+	if x != nil && x.Copyright != nil {
+		return *x.Copyright
+	}
+	return ""
+}
+
+func (x *Audio) GetDisc() int32 {
+	if x != nil && x.Disc != nil {
+		return *x.Disc
+	}
+	return 0
+}
+
+func (x *Audio) GetDiscCount() int32 {
+	if x != nil && x.DiscCount != nil {
+		return *x.DiscCount
+	}
+	return 0
+}
+
+func (x *Audio) GetDuration() int64 {
+	if x != nil && x.Duration != nil {
+		return *x.Duration
+	}
+	return 0
+}
+
+func (x *Audio) GetGenre() string {
+	if x != nil && x.Genre != nil {
+		return *x.Genre
+	}
+	return ""
+}
+
+func (x *Audio) GetHasDrm() bool {
+	if x != nil && x.HasDrm != nil {
+		return *x.HasDrm
+	}
+	return false
+}
+
+func (x *Audio) GetIsVariableBitrate() bool {
+	if x != nil && x.IsVariableBitrate != nil {
+		return *x.IsVariableBitrate
+	}
+	return false
+}
+
+func (x *Audio) GetTitle() string {
+	if x != nil && x.Title != nil {
+		return *x.Title
+	}
+	return ""
+}
+
+func (x *Audio) GetTrack() int32 {
+	if x != nil && x.Track != nil {
+		return *x.Track
+	}
+	return 0
+}
+
+func (x *Audio) GetTrackCount() int32 {
+	if x != nil && x.TrackCount != nil {
+		return *x.TrackCount
+	}
+	return 0
+}
+
+func (x *Audio) GetYear() int32 {
+	if x != nil && x.Year != nil {
+		return *x.Year
+	}
+	return 0
+}
+
 type Entity struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -158,12 +325,13 @@ type Entity struct {
 	ParentId         *ResourceID            `protobuf:"bytes,12,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"`
 	Tags             []string               `protobuf:"bytes,13,rep,name=tags,proto3" json:"tags,omitempty"`
 	Highlights       string                 `protobuf:"bytes,14,opt,name=highlights,proto3" json:"highlights,omitempty"`
+	Audio            *Audio                 `protobuf:"bytes,15,opt,name=audio,proto3" json:"audio,omitempty"`
 }
 
 func (x *Entity) Reset() {
 	*x = Entity{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2]
+		mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -176,7 +344,7 @@ func (x *Entity) String() string {
 func (*Entity) ProtoMessage() {}
 
 func (x *Entity) ProtoReflect() protoreflect.Message {
-	mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2]
+	mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -189,7 +357,7 @@ func (x *Entity) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Entity.ProtoReflect.Descriptor instead.
 func (*Entity) Descriptor() ([]byte, []int) {
-	return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{2}
+	return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *Entity) GetRef() *Reference {
@@ -290,6 +458,13 @@ func (x *Entity) GetHighlights() string {
 	return ""
 }
 
+func (x *Entity) GetAudio() *Audio {
+	if x != nil {
+		return x.Audio
+	}
+	return nil
+}
+
 type Match struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -304,7 +479,7 @@ type Match struct {
 func (x *Match) Reset() {
 	*x = Match{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3]
+		mi := &file_ocis_messages_search_v0_search_proto_msgTypes[4]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -317,7 +492,7 @@ func (x *Match) String() string {
 func (*Match) ProtoMessage() {}
 
 func (x *Match) ProtoReflect() protoreflect.Message {
-	mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3]
+	mi := &file_ocis_messages_search_v0_search_proto_msgTypes[4]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -330,7 +505,7 @@ func (x *Match) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Match.ProtoReflect.Descriptor instead.
 func (*Match) Descriptor() ([]byte, []int) {
-	return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{3}
+	return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *Match) GetEntity() *Entity {
@@ -368,50 +543,98 @@ var file_ocis_messages_search_v0_search_proto_rawDesc = []byte{
 	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
 	0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x0a, 0x72, 0x65,
 	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x82, 0x04, 0x0a,
-	0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73,
-	0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52,
-	0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x33, 0x0a,
-	0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x63, 0x69, 0x73,
-	0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
-	0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x02,
-	0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x04,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69,
-	0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x48,
-	0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f,
-	0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
-	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69,
-	0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65,
-	0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d,
-	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d,
-	0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
-	0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64,
-	0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65,
-	0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f,
-	0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x68,
-	0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x70,
-	0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23,
-	0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73,
-	0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
-	0x65, 0x49, 0x44, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a,
-	0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67,
-	0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x18,
-	0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74,
-	0x73, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e,
-	0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69,
-	0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
-	0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74,
-	0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74,
-	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
-	0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65,
-	0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61,
-	0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xcf, 0x05, 0x0a,
+	0x05, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x12, 0x19, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x88, 0x01,
+	0x01, 0x12, 0x25, 0x0a, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41,
+	0x72, 0x74, 0x69, 0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69,
+	0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69,
+	0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x62, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x03, 0x52, 0x07, 0x62, 0x69, 0x74, 0x72, 0x61, 0x74,
+	0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x72,
+	0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f,
+	0x73, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x63, 0x6f, 0x70, 0x79, 0x72,
+	0x69, 0x67, 0x68, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, 0x09, 0x63, 0x6f,
+	0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x64, 0x69,
+	0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x06, 0x52, 0x04, 0x64, 0x69, 0x73, 0x63,
+	0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74,
+	0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, 0x09, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f,
+	0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x48, 0x08, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65,
+	0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x09, 0x52, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, 0x88,
+	0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x18, 0x0b, 0x20, 0x01,
+	0x28, 0x08, 0x48, 0x0a, 0x52, 0x06, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x88, 0x01, 0x01, 0x12,
+	0x31, 0x0a, 0x11, 0x69, 0x73, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74,
+	0x72, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x11, 0x69, 0x73,
+	0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65, 0x88,
+	0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28,
+	0x09, 0x48, 0x0c, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a,
+	0x05, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0d, 0x52, 0x05,
+	0x74, 0x72, 0x61, 0x63, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x63,
+	0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0e, 0x52, 0x0a,
+	0x74, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a,
+	0x04, 0x79, 0x65, 0x61, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0f, 0x52, 0x04, 0x79,
+	0x65, 0x61, 0x72, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d,
+	0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74,
+	0x42, 0x09, 0x0a, 0x07, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
+	0x62, 0x69, 0x74, 0x72, 0x61, 0x74, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x6d, 0x70,
+	0x6f, 0x73, 0x65, 0x72, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x70, 0x79, 0x72, 0x69,
+	0x67, 0x68, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x42, 0x0c, 0x0a, 0x0a,
+	0x5f, 0x64, 0x69, 0x73, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64,
+	0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x65, 0x6e, 0x72,
+	0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x61, 0x73, 0x44, 0x72, 0x6d, 0x42, 0x14, 0x0a, 0x12,
+	0x5f, 0x69, 0x73, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x69, 0x74, 0x72, 0x61,
+	0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x42, 0x08, 0x0a, 0x06,
+	0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b,
+	0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x79, 0x65, 0x61, 0x72, 0x22, 0xb8,
+	0x04, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, 0x72, 0x65, 0x66,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65,
+	0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30,
+	0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12,
+	0x33, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x63,
+	0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72,
+	0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44,
+	0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04,
+	0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
+	0x12, 0x48, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65,
+	0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
+	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x6f,
+	0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69,
+	0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d,
+	0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69,
+	0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65,
+	0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70,
+	0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a,
+	0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07,
+	0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65,
+	0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
+	0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a,
+	0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x23, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
+	0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12,
+	0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74,
+	0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74,
+	0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67,
+	0x68, 0x74, 0x73, 0x12, 0x34, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
+	0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x75, 0x64,
+	0x69, 0x6f, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, 0x74,
+	0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
+	0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74,
+	0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73,
+	0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72,
+	0x65, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
+	0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x76, 0x32,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63,
+	0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72,
+	0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -426,26 +649,28 @@ func file_ocis_messages_search_v0_search_proto_rawDescGZIP() []byte {
 	return file_ocis_messages_search_v0_search_proto_rawDescData
 }
 
-var file_ocis_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_ocis_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
 var file_ocis_messages_search_v0_search_proto_goTypes = []interface{}{
 	(*ResourceID)(nil),            // 0: ocis.messages.search.v0.ResourceID
 	(*Reference)(nil),             // 1: ocis.messages.search.v0.Reference
-	(*Entity)(nil),                // 2: ocis.messages.search.v0.Entity
-	(*Match)(nil),                 // 3: ocis.messages.search.v0.Match
-	(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
+	(*Audio)(nil),                 // 2: ocis.messages.search.v0.Audio
+	(*Entity)(nil),                // 3: ocis.messages.search.v0.Entity
+	(*Match)(nil),                 // 4: ocis.messages.search.v0.Match
+	(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
 }
 var file_ocis_messages_search_v0_search_proto_depIdxs = []int32{
 	0, // 0: ocis.messages.search.v0.Reference.resource_id:type_name -> ocis.messages.search.v0.ResourceID
 	1, // 1: ocis.messages.search.v0.Entity.ref:type_name -> ocis.messages.search.v0.Reference
 	0, // 2: ocis.messages.search.v0.Entity.id:type_name -> ocis.messages.search.v0.ResourceID
-	4, // 3: ocis.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp
+	5, // 3: ocis.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp
 	0, // 4: ocis.messages.search.v0.Entity.parent_id:type_name -> ocis.messages.search.v0.ResourceID
-	2, // 5: ocis.messages.search.v0.Match.entity:type_name -> ocis.messages.search.v0.Entity
-	6, // [6:6] is the sub-list for method output_type
-	6, // [6:6] is the sub-list for method input_type
-	6, // [6:6] is the sub-list for extension type_name
-	6, // [6:6] is the sub-list for extension extendee
-	0, // [0:6] is the sub-list for field type_name
+	2, // 5: ocis.messages.search.v0.Entity.audio:type_name -> ocis.messages.search.v0.Audio
+	3, // 6: ocis.messages.search.v0.Match.entity:type_name -> ocis.messages.search.v0.Entity
+	7, // [7:7] is the sub-list for method output_type
+	7, // [7:7] is the sub-list for method input_type
+	7, // [7:7] is the sub-list for extension type_name
+	7, // [7:7] is the sub-list for extension extendee
+	0, // [0:7] is the sub-list for field type_name
 }
 
 func init() { file_ocis_messages_search_v0_search_proto_init() }
@@ -479,7 +704,7 @@ func file_ocis_messages_search_v0_search_proto_init() {
 			}
 		}
 		file_ocis_messages_search_v0_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*Entity); i {
+			switch v := v.(*Audio); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -491,6 +716,18 @@ func file_ocis_messages_search_v0_search_proto_init() {
 			}
 		}
 		file_ocis_messages_search_v0_search_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Entity); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_ocis_messages_search_v0_search_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Match); i {
 			case 0:
 				return &v.state
@@ -503,13 +740,14 @@ func file_ocis_messages_search_v0_search_proto_init() {
 			}
 		}
 	}
+	file_ocis_messages_search_v0_search_proto_msgTypes[2].OneofWrappers = []interface{}{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_ocis_messages_search_v0_search_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   4,
+			NumMessages:   5,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.web.go b/protogen/gen/ocis/messages/search/v0/search.pb.web.go
index 7055e834977..1ed7a0bf377 100644
--- a/protogen/gen/ocis/messages/search/v0/search.pb.web.go
+++ b/protogen/gen/ocis/messages/search/v0/search.pb.web.go
@@ -82,6 +82,42 @@ func (m *Reference) UnmarshalJSON(b []byte) error {
 
 var _ json.Unmarshaler = (*Reference)(nil)
 
+// AudioJSONMarshaler describes the default jsonpb.Marshaler used by all
+// instances of Audio. This struct is safe to replace or modify but
+// should not be done so concurrently.
+var AudioJSONMarshaler = new(jsonpb.Marshaler)
+
+// MarshalJSON satisfies the encoding/json Marshaler interface. This method
+// uses the more correct jsonpb package to correctly marshal the message.
+func (m *Audio) MarshalJSON() ([]byte, error) {
+	if m == nil {
+		return json.Marshal(nil)
+	}
+
+	buf := &bytes.Buffer{}
+
+	if err := AudioJSONMarshaler.Marshal(buf, m); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+var _ json.Marshaler = (*Audio)(nil)
+
+// AudioJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all
+// instances of Audio. This struct is safe to replace or modify but
+// should not be done so concurrently.
+var AudioJSONUnmarshaler = new(jsonpb.Unmarshaler)
+
+// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method
+// uses the more correct jsonpb package to correctly unmarshal the message.
+func (m *Audio) UnmarshalJSON(b []byte) error {
+	return AudioJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m)
+}
+
+var _ json.Unmarshaler = (*Audio)(nil)
+
 // EntityJSONMarshaler describes the default jsonpb.Marshaler used by all
 // instances of Entity. This struct is safe to replace or modify but
 // should not be done so concurrently.
diff --git a/protogen/gen/ocis/messages/settings/v0/settings.pb.go b/protogen/gen/ocis/messages/settings/v0/settings.pb.go
index 9f0f3d92c64..bae3a9c8710 100644
--- a/protogen/gen/ocis/messages/settings/v0/settings.pb.go
+++ b/protogen/gen/ocis/messages/settings/v0/settings.pb.go
@@ -489,13 +489,13 @@ type Bundle struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Id          string      `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                                                 // @gotags: yaml:"id"
-	Name        string      `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`                                             // @gotags: yaml:"name"
-	Type        Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=ocis.messages.settings.v0.Bundle_Type" json:"type,omitempty"` // @gotags: yaml:"type"
-	Extension   string      `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty"`                                   // @gotags: yaml:"extension"
-	DisplayName string      `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`            // @gotags: yaml:"display_name"
-	Settings    []*Setting  `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty"`                                     // @gotags: yaml:"settings"
-	Resource    *Resource   `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty"`                                     // @gotags: yaml:"resource"
+	Id          string      `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"`                                                 // @gotags: yaml:"id"
+	Name        string      `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"`                                             // @gotags: yaml:"name"
+	Type        Bundle_Type `protobuf:"varint,3,opt,name=type,proto3,enum=ocis.messages.settings.v0.Bundle_Type" json:"type,omitempty" yaml:"type"` // @gotags: yaml:"type"
+	Extension   string      `protobuf:"bytes,4,opt,name=extension,proto3" json:"extension,omitempty" yaml:"extension"`                                   // @gotags: yaml:"extension"
+	DisplayName string      `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"`            // @gotags: yaml:"display_name"
+	Settings    []*Setting  `protobuf:"bytes,6,rep,name=settings,proto3" json:"settings,omitempty" yaml:"settings"`                                     // @gotags: yaml:"settings"
+	Resource    *Resource   `protobuf:"bytes,7,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"`                                     // @gotags: yaml:"resource"
 }
 
 func (x *Bundle) Reset() {
@@ -584,10 +584,10 @@ type Setting struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Id          string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                                      // @gotags: yaml:"id"
-	Name        string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`                                  // @gotags: yaml:"name"
-	DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // @gotags: yaml:"display_name"
-	Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`                    // @gotags: yaml:"description"
+	Id          string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"`                                      // @gotags: yaml:"id"
+	Name        string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty" yaml:"name"`                                  // @gotags: yaml:"name"
+	DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty" yaml:"display_name"` // @gotags: yaml:"display_name"
+	Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" yaml:"description"`                    // @gotags: yaml:"description"
 	// Types that are assignable to Value:
 	//
 	//	*Setting_IntValue
@@ -597,7 +597,7 @@ type Setting struct {
 	//	*Setting_MultiChoiceValue
 	//	*Setting_PermissionValue
 	Value    isSetting_Value `protobuf_oneof:"value"`
-	Resource *Resource       `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty"` // @gotags: yaml:"resource"
+	Resource *Resource       `protobuf:"bytes,11,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"` // @gotags: yaml:"resource"
 }
 
 func (x *Setting) Reset() {
@@ -721,27 +721,27 @@ type isSetting_Value interface {
 }
 
 type Setting_IntValue struct {
-	IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
+	IntValue *Int `protobuf:"bytes,5,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
 }
 
 type Setting_StringValue struct {
-	StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
+	StringValue *String `protobuf:"bytes,6,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
 }
 
 type Setting_BoolValue struct {
-	BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
+	BoolValue *Bool `protobuf:"bytes,7,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
 }
 
 type Setting_SingleChoiceValue struct {
-	SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof"` // @gotags: yaml:"single_choice_value"
+	SingleChoiceValue *SingleChoiceList `protobuf:"bytes,8,opt,name=single_choice_value,json=singleChoiceValue,proto3,oneof" yaml:"single_choice_value"` // @gotags: yaml:"single_choice_value"
 }
 
 type Setting_MultiChoiceValue struct {
-	MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof"` // @gotags: yaml:"multi_choice_value"
+	MultiChoiceValue *MultiChoiceList `protobuf:"bytes,9,opt,name=multi_choice_value,json=multiChoiceValue,proto3,oneof" yaml:"multi_choice_value"` // @gotags: yaml:"multi_choice_value"
 }
 
 type Setting_PermissionValue struct {
-	PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof"` // @gotags: yaml:"permission_value"
+	PermissionValue *Permission `protobuf:"bytes,10,opt,name=permission_value,json=permissionValue,proto3,oneof" yaml:"permission_value"` // @gotags: yaml:"permission_value"
 }
 
 func (*Setting_IntValue) isSetting_Value() {}
@@ -761,11 +761,11 @@ type Int struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Default     int64  `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"`        // @gotags: yaml:"default"
-	Min         int64  `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty"`                // @gotags: yaml:"min"
-	Max         int64  `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty"`                // @gotags: yaml:"max"
-	Step        int64  `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty"`              // @gotags: yaml:"step"
-	Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"` // @gotags: yaml:"placeholder"
+	Default     int64  `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"`        // @gotags: yaml:"default"
+	Min         int64  `protobuf:"varint,2,opt,name=min,proto3" json:"min,omitempty" yaml:"min"`                // @gotags: yaml:"min"
+	Max         int64  `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty" yaml:"max"`                // @gotags: yaml:"max"
+	Step        int64  `protobuf:"varint,4,opt,name=step,proto3" json:"step,omitempty" yaml:"step"`              // @gotags: yaml:"step"
+	Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"` // @gotags: yaml:"placeholder"
 }
 
 func (x *Int) Reset() {
@@ -840,11 +840,11 @@ type String struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Default     string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty"`                       // @gotags: yaml:"default"
-	Required    bool   `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"`                    // @gotags: yaml:"required"
-	MinLength   int32  `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty"` // @gotags: yaml:"min_length"
-	MaxLength   int32  `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty"` // @gotags: yaml:"max_length"
-	Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty"`               // @gotags: yaml:"placeholder"
+	Default     string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"`                       // @gotags: yaml:"default"
+	Required    bool   `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty" yaml:"required"`                    // @gotags: yaml:"required"
+	MinLength   int32  `protobuf:"varint,3,opt,name=min_length,json=minLength,proto3" json:"min_length,omitempty" yaml:"min_length"` // @gotags: yaml:"min_length"
+	MaxLength   int32  `protobuf:"varint,4,opt,name=max_length,json=maxLength,proto3" json:"max_length,omitempty" yaml:"max_length"` // @gotags: yaml:"max_length"
+	Placeholder string `protobuf:"bytes,5,opt,name=placeholder,proto3" json:"placeholder,omitempty" yaml:"placeholder"`               // @gotags: yaml:"placeholder"
 }
 
 func (x *String) Reset() {
@@ -919,8 +919,8 @@ type Bool struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Default bool   `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty"` // @gotags: yaml:"default"
-	Label   string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"`      // @gotags: yaml:"label"
+	Default bool   `protobuf:"varint,1,opt,name=default,proto3" json:"default,omitempty" yaml:"default"` // @gotags: yaml:"default"
+	Label   string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty" yaml:"label"`      // @gotags: yaml:"label"
 }
 
 func (x *Bool) Reset() {
@@ -974,7 +974,7 @@ type SingleChoiceList struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options"
+	Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options"
 }
 
 func (x *SingleChoiceList) Reset() {
@@ -1021,7 +1021,7 @@ type MultiChoiceList struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty"` // @gotags: yaml:"options"
+	Options []*ListOption `protobuf:"bytes,1,rep,name=options,proto3" json:"options,omitempty" yaml:"options"` // @gotags: yaml:"options"
 }
 
 func (x *MultiChoiceList) Reset() {
@@ -1068,9 +1068,9 @@ type ListOption struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Value        *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`                                   // @gotags: yaml:"value"
-	Default      bool             `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty"`                              // @gotags: yaml:"default"
-	DisplayValue string           `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty"` // @gotags: yaml:"display_value"
+	Value        *ListOptionValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty" yaml:"value"`                                   // @gotags: yaml:"value"
+	Default      bool             `protobuf:"varint,2,opt,name=default,proto3" json:"default,omitempty" yaml:"default"`                              // @gotags: yaml:"default"
+	DisplayValue string           `protobuf:"bytes,3,opt,name=display_value,json=displayValue,proto3" json:"display_value,omitempty" yaml:"display_value"` // @gotags: yaml:"display_value"
 }
 
 func (x *ListOption) Reset() {
@@ -1131,8 +1131,8 @@ type Permission struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Operation  Permission_Operation  `protobuf:"varint,1,opt,name=operation,proto3,enum=ocis.messages.settings.v0.Permission_Operation" json:"operation,omitempty"`    // @gotags: yaml:"operation"
-	Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=ocis.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty"` // @gotags: yaml:"constraint"
+	Operation  Permission_Operation  `protobuf:"varint,1,opt,name=operation,proto3,enum=ocis.messages.settings.v0.Permission_Operation" json:"operation,omitempty" yaml:"operation"`    // @gotags: yaml:"operation"
+	Constraint Permission_Constraint `protobuf:"varint,2,opt,name=constraint,proto3,enum=ocis.messages.settings.v0.Permission_Constraint" json:"constraint,omitempty" yaml:"constraint"` // @gotags: yaml:"constraint"
 }
 
 func (x *Permission) Reset() {
@@ -1187,12 +1187,12 @@ type Value struct {
 	unknownFields protoimpl.UnknownFields
 
 	// id is the id of the Value. It is generated on saving it.
-	Id       string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                             // @gotags: yaml:"id"
-	BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty"` // @gotags: yaml:"bundle_id"
+	Id       string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" yaml:"id"`                             // @gotags: yaml:"id"
+	BundleId string `protobuf:"bytes,2,opt,name=bundle_id,json=bundleId,proto3" json:"bundle_id,omitempty" yaml:"bundle_id"` // @gotags: yaml:"bundle_id"
 	// setting_id is the id of the setting from within its bundle.
-	SettingId   string    `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty"`       // @gotags: yaml:"setting_id"
-	AccountUuid string    `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty"` // @gotags: yaml:"account_uuid"
-	Resource    *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty"`                          // @gotags: yaml:"resource"
+	SettingId   string    `protobuf:"bytes,3,opt,name=setting_id,json=settingId,proto3" json:"setting_id,omitempty" yaml:"setting_id"`       // @gotags: yaml:"setting_id"
+	AccountUuid string    `protobuf:"bytes,4,opt,name=account_uuid,json=accountUuid,proto3" json:"account_uuid,omitempty" yaml:"account_uuid"` // @gotags: yaml:"account_uuid"
+	Resource    *Resource `protobuf:"bytes,5,opt,name=resource,proto3" json:"resource,omitempty" yaml:"resource"`                          // @gotags: yaml:"resource"
 	// Types that are assignable to Value:
 	//
 	//	*Value_BoolValue
@@ -1309,19 +1309,19 @@ type isValue_Value interface {
 }
 
 type Value_BoolValue struct {
-	BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof"` // @gotags: yaml:"bool_value"
+	BoolValue bool `protobuf:"varint,6,opt,name=bool_value,json=boolValue,proto3,oneof" yaml:"bool_value"` // @gotags: yaml:"bool_value"
 }
 
 type Value_IntValue struct {
-	IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
+	IntValue int64 `protobuf:"varint,7,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
 }
 
 type Value_StringValue struct {
-	StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
+	StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
 }
 
 type Value_ListValue struct {
-	ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof"` // @gotags: yaml:"list_value"
+	ListValue *ListValue `protobuf:"bytes,9,opt,name=list_value,json=listValue,proto3,oneof" yaml:"list_value"` // @gotags: yaml:"list_value"
 }
 
 func (*Value_BoolValue) isValue_Value() {}
@@ -1337,7 +1337,7 @@ type ListValue struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` // @gotags: yaml:"values"
+	Values []*ListOptionValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" yaml:"values"` // @gotags: yaml:"values"
 }
 
 func (x *ListValue) Reset() {
@@ -1449,11 +1449,11 @@ type isListOptionValue_Option interface {
 }
 
 type ListOptionValue_StringValue struct {
-	StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` // @gotags: yaml:"string_value"
+	StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof" yaml:"string_value"` // @gotags: yaml:"string_value"
 }
 
 type ListOptionValue_IntValue struct {
-	IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` // @gotags: yaml:"int_value"
+	IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof" yaml:"int_value"` // @gotags: yaml:"int_value"
 }
 
 func (*ListOptionValue_StringValue) isListOptionValue_Option() {}
diff --git a/protogen/gen/ocis/services/search/v0/search.swagger.json b/protogen/gen/ocis/services/search/v0/search.swagger.json
index 267235414fd..6fce96faa1f 100644
--- a/protogen/gen/ocis/services/search/v0/search.swagger.json
+++ b/protogen/gen/ocis/services/search/v0/search.swagger.json
@@ -157,6 +157,66 @@
         }
       }
     },
+    "v0Audio": {
+      "type": "object",
+      "properties": {
+        "album": {
+          "type": "string"
+        },
+        "albumArtist": {
+          "type": "string"
+        },
+        "artist": {
+          "type": "string"
+        },
+        "bitrate": {
+          "type": "string",
+          "format": "int64"
+        },
+        "composers": {
+          "type": "string"
+        },
+        "copyright": {
+          "type": "string"
+        },
+        "disc": {
+          "type": "integer",
+          "format": "int32"
+        },
+        "discCount": {
+          "type": "integer",
+          "format": "int32"
+        },
+        "duration": {
+          "type": "string",
+          "format": "int64"
+        },
+        "genre": {
+          "type": "string"
+        },
+        "hasDrm": {
+          "type": "boolean"
+        },
+        "isVariableBitrate": {
+          "type": "boolean"
+        },
+        "title": {
+          "type": "string"
+        },
+        "track": {
+          "type": "integer",
+          "format": "int32"
+        },
+        "trackCount": {
+          "type": "integer",
+          "format": "int32"
+        },
+        "year": {
+          "type": "integer",
+          "format": "int32"
+        }
+      }
+    },
     "v0Entity": {
       "type": "object",
       "properties": {
@@ -207,6 +267,9 @@
         },
         "highlights": {
           "type": "string"
+        },
+        "audio": {
+          "$ref": "#/definitions/v0Audio"
         }
       }
     },
diff --git a/protogen/proto/ocis/messages/search/v0/search.proto b/protogen/proto/ocis/messages/search/v0/search.proto
index 442eb502d19..034d7f85e4b 100644
--- a/protogen/proto/ocis/messages/search/v0/search.proto
+++ b/protogen/proto/ocis/messages/search/v0/search.proto
@@ -17,6 +17,25 @@ message Reference {
 	string path = 2;
 }
 
+message Audio {
+	optional string album = 1;
+	optional string albumArtist = 2;
+	optional string artist = 3;
+	optional int64 bitrate = 4;
+	optional string composers = 5;
+	optional string copyright = 6;
+	optional int32 disc = 7;
+	optional int32 discCount = 8;
+	optional int64 duration = 9;
+	optional string genre = 10;
+	optional bool hasDrm = 11;
+	optional bool isVariableBitrate = 12;
+	optional string title = 13;
+	optional int32 track = 14;
+	optional int32 trackCount = 15;
+	optional int32 year = 16;
+}
+
 message Entity {
 	Reference ref = 1;
 	ResourceID id = 2;
@@ -32,6 +51,7 @@ message Entity {
 	ResourceID parent_id = 12;
 	repeated string tags = 13;
 	string highlights = 14;
+	Audio audio = 15;
 }
 
 message Match {
diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go
index 0e31bb677d0..f8028bc711a 100644
--- a/services/graph/pkg/service/v0/driveitems.go
+++ b/services/graph/pkg/service/v0/driveitems.go
@@ -7,6 +7,9 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"reflect"
+	"strconv"
+	"strings"
 	"time"
 
 	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -17,6 +20,7 @@ import (
 	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/go-chi/render"
 	libregraph "github.com/owncloud/libre-graph-api-go"
+	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
 	"golang.org/x/crypto/sha3"
 )
@@ -87,7 +91,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	files, err := formatDriveItems(lRes.Infos)
+	files, err := formatDriveItems(g.logger, lRes.Infos)
 	if err != nil {
 		g.logger.Error().Err(err).Msg("error encoding response as json")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -152,7 +156,7 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
 		return
 	}
-	driveItem, err := cs3ResourceToDriveItem(res.Info)
+	driveItem, err := cs3ResourceToDriveItem(g.logger, res.Info)
 	if err != nil {
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
@@ -220,7 +224,7 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	files, err := formatDriveItems(res.Infos)
+	files, err := formatDriveItems(g.logger, res.Infos)
 	if err != nil {
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
@@ -244,7 +248,7 @@ func (g Graph) getDriveItem(ctx context.Context, ref storageprovider.Reference)
 		refStr, _ := storagespace.FormatReference(&ref)
 		return nil, fmt.Errorf("could not stat %s: %s", refStr, res.Status.Message)
 	}
-	return cs3ResourceToDriveItem(res.Info)
+	return cs3ResourceToDriveItem(g.logger, res.Info)
 }
 
 func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.ResourceId, baseURL *url.URL) (*libregraph.RemoteItem, error) {
@@ -285,10 +289,10 @@ func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.Resource
 	return item, nil
 }
 
-func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
+func formatDriveItems(logger *log.Logger, mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
 	responses := make([]*libregraph.DriveItem, 0, len(mds))
 	for i := range mds {
-		res, err := cs3ResourceToDriveItem(mds[i])
+		res, err := cs3ResourceToDriveItem(logger, mds[i])
 		if err != nil {
 			return nil, err
 		}
@@ -302,7 +306,7 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time {
 	return time.Unix(int64(t.Seconds), int64(t.Nanos))
 }
 
-func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
+func cs3ResourceToDriveItem(logger *log.Logger, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
 	size := new(int64)
 	*size = int64(res.Size) // TODO lurking overflow: make size of libregraph drive item use uint64
 
@@ -321,6 +325,13 @@ func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.Driv
 		lastModified := cs3TimestampToTime(res.Mtime)
 		driveItem.LastModifiedDateTime = &lastModified
 	}
+	if res.ParentId != nil {
+		parentRef := libregraph.NewItemReference()
+		parentRef.SetDriveType(res.Space.SpaceType)
+		parentRef.SetDriveId(storagespace.FormatStorageID(res.ParentId.StorageId, res.ParentId.SpaceId))
+		parentRef.SetId(storagespace.FormatResourceID(*res.ParentId))
+		driveItem.ParentReference = parentRef
+	}
 	if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE && res.MimeType != "" {
 		// We cannot use a libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ...
 		driveItem.File = &libregraph.OpenGraphFile{
@@ -330,9 +341,79 @@ func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.Driv
 	if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER {
 		driveItem.Folder = &libregraph.Folder{}
 	}
+
+	driveItem.Audio = cs3ResourceToDriveItemAudioFacet(logger, res)
+
 	return driveItem, nil
 }
 
+func cs3ResourceToDriveItemAudioFacet(logger *log.Logger, res *storageprovider.ResourceInfo) *libregraph.Audio {
+	if !strings.HasPrefix(res.MimeType, "audio/") {
+		return nil
+	}
+
+	k := res.ArbitraryMetadata.Metadata
+	if k == nil {
+		return nil
+	}
+
+	var audio = &libregraph.Audio{}
+	if ok := unmarshalStringMap(logger, audio, k, "libre.graph.audio."); ok {
+		return audio
+	}
+
+	return nil
+}
+
+func getFieldName(structField reflect.StructField) string {
+	tag := structField.Tag.Get("json")
+	if tag == "" {
+		return structField.Name
+	}
+
+	return strings.Split(tag, ",")[0]
+}
+
+func unmarshalStringMap(logger *log.Logger, out any, flatMap map[string]string, prefix string) bool {
+	nonEmpty := false
+	obj := reflect.ValueOf(out).Elem()
+	for i := 0; i < obj.NumField(); i++ {
+		field := obj.Field(i)
+		structField := obj.Type().Field(i)
+		mapKey := prefix + getFieldName(structField)
+
+		if value, ok := flatMap[mapKey]; ok {
+			if field.Kind() == reflect.Ptr {
+				newValue := reflect.New(field.Type().Elem())
+				var tmp any
+				var err error
+				switch t := newValue.Type().Elem().Kind(); t {
+				case reflect.String:
+					tmp = value
+				case reflect.Int32:
+					tmp, err = strconv.ParseInt(value, 10, 32)
+				case reflect.Int64:
+					tmp, err = strconv.ParseInt(value, 10, 64)
+				case reflect.Bool:
+					tmp, err = strconv.ParseBool(value)
+				default:
+					err = errors.New("unsupported type")
+					logger.Error().Err(err).Str("type", t.String()).Str("mapKey", mapKey).Msg("target field type for value of mapKey is not supported")
+				}
+				if err != nil {
+					logger.Error().Err(err).Str("mapKey", mapKey).Msg("unmarshalling failed")
+					continue
+				}
+				newValue.Elem().Set(reflect.ValueOf(tmp).Convert(field.Type().Elem()))
+				field.Set(newValue)
+				nonEmpty = true
+			}
+		}
+	}
+
+	return nonEmpty
+}
+
 func cs3ResourceToRemoteItem(res *storageprovider.ResourceInfo) (*libregraph.RemoteItem, error) {
 	size := new(int64)
 	*size = int64(res.Size) // TODO lurking overflow: make size of libregraph drive item use uint64
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index 88ba09c0891..c48bbee037b 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -96,6 +96,7 @@ type Service interface {
 	GetDrives(w http.ResponseWriter, r *http.Request)
 	GetSingleDrive(w http.ResponseWriter, r *http.Request)
 	GetAllDrives(w http.ResponseWriter, r *http.Request)
+	GetSharedByMe(w http.ResponseWriter, r *http.Request)
 	CreateDrive(w http.ResponseWriter, r *http.Request)
 	UpdateDrive(w http.ResponseWriter, r *http.Request)
 	DeleteDrive(w http.ResponseWriter, r *http.Request)
@@ -199,6 +200,9 @@ func NewService(opts ...Option) (Graph, error) {
 
 	m.Route(options.Config.HTTP.Root, func(r chi.Router) {
 		r.Use(middleware.StripSlashes)
+		r.Route("/v1beta1", func(r chi.Router) {
+			r.Get("/me/drives/sharedByMe", svc.GetSharedByMe)
+		})
 		r.Route("/v1.0", func(r chi.Router) {
 			r.Route("/extensions/org.libregraph", func(r chi.Router) {
 				r.Get("/tags", svc.GetTags)
@@ -212,7 +216,9 @@ func NewService(opts ...Option) (Graph, error) {
 			r.Route("/me", func(r chi.Router) {
 				r.Get("/", svc.GetMe)
 				r.Get("/drive", svc.GetUserDrive)
-				r.Get("/drives", svc.GetDrives)
+				r.Route("/drives", func(r chi.Router) {
+					r.Get("/", svc.GetDrives)
+				})
 				r.Get("/drive/root/children", svc.GetRootDriveChildren)
 				r.Post("/changePassword", svc.ChangeOwnPassword)
 			})
diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go
new file mode 100644
index 00000000000..71146c45070
--- /dev/null
+++ b/services/graph/pkg/service/v0/sharedbyme.go
@@ -0,0 +1,242 @@
+package svc
+
+import (
+	"context"
+	"net/http"
+	"net/url"
+	"path"
+
+	group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
+	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
+	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
+	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
+	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/share"
+	"github.com/cs3org/reva/v2/pkg/storagespace"
+	revautils "github.com/cs3org/reva/v2/pkg/utils"
+	"github.com/go-chi/render"
+	libregraph "github.com/owncloud/libre-graph-api-go"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
+)
+
+type driveItemsByResourceID map[string]libregraph.DriveItem
+
+// GetSharedByMe implements the Service interface (/me/drives/sharedByMe endpoint)
+func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) {
+	g.logger.Debug().Msg("Calling GetRootDriveChildren")
+	ctx := r.Context()
+
+	driveItems := make(driveItemsByResourceID)
+	var err error
+	driveItems, err = g.listUserShares(ctx, driveItems)
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+
+	driveItems, err = g.listPublicShares(ctx, driveItems)
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+
+	res := make([]libregraph.DriveItem, 0, len(driveItems))
+	for _, v := range driveItems {
+		res = append(res, v)
+	}
+
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, &ListResponse{Value: res})
+}
+
+func (g Graph) listUserShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		g.logger.Error().Err(err).Msg("could not select next gateway client")
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+
+	filters := []*collaboration.Filter{
+		share.UserGranteeFilter(),
+		share.GroupGranteeFilter(),
+	}
+	lsUserSharesRequest := collaboration.ListSharesRequest{
+		Filters: filters,
+	}
+
+	lsUserSharesResponse, err := gatewayClient.ListShares(ctx, &lsUserSharesRequest)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	if statusCode := lsUserSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
+		return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsUserSharesResponse.Status.Message)
+	}
+	driveItems, err = g.cs3UserSharesToDriveItems(ctx, lsUserSharesResponse.Shares, driveItems)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	return driveItems, nil
+}
+
+func (g Graph) listPublicShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
+
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		g.logger.Error().Err(err).Msg("could not select next gateway client")
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+
+	filters := []*link.ListPublicSharesRequest_Filter{}
+
+	req := link.ListPublicSharesRequest{
+		Filters: filters,
+	}
+
+	lsPublicSharesResponse, err := gatewayClient.ListPublicShares(ctx, &req)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	if statusCode := lsPublicSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
+		return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsPublicSharesResponse.Status.Message)
+	}
+	driveItems, err = g.cs3PublicSharesToDriveItems(ctx, lsPublicSharesResponse.Share, driveItems)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	return driveItems, nil
+
+}
+
+func (g Graph) cs3UserSharesToDriveItems(ctx context.Context, shares []*collaboration.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
+	for _, s := range shares {
+		g.logger.Debug().Interface("CS3 UserShare", s).Msg("Got Share")
+		resIDStr := storagespace.FormatResourceID(*s.ResourceId)
+		item, ok := driveItems[resIDStr]
+		if !ok {
+			itemptr, err := g.getDriveItem(ctx, storageprovider.Reference{ResourceId: s.ResourceId})
+			if err != nil {
+				g.logger.Debug().Err(err).Interface("Share", s.ResourceId).Msg("could not stat share, skipping")
+				continue
+			}
+			item = *itemptr
+		}
+		perm := libregraph.Permission{}
+		perm.SetRoles([]string{})
+		perm.SetId(s.Id.OpaqueId)
+		grantedTo := libregraph.SharePointIdentitySet{}
+		switch s.Grantee.Type {
+		case storageprovider.GranteeType_GRANTEE_TYPE_USER:
+			gatewayClient, err := g.gatewaySelector.Next()
+			if err != nil {
+				g.logger.Error().Err(err).Msg("could not select next gateway client")
+				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+			}
+			user := libregraph.NewIdentityWithDefaults()
+			user.SetId(s.Grantee.GetUserId().GetOpaqueId())
+			cs3User, err := revautils.GetUser(s.GetGrantee().GetUserId(), gatewayClient)
+			switch {
+			case revautils.IsErrNotFound(err):
+				g.logger.Warn().Str("userid", s.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id")
+				// User does not seem to exist anymore, don't add a permission for this
+				continue
+			case err != nil:
+				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+			default:
+				user.SetDisplayName(cs3User.GetDisplayName())
+				grantedTo.SetUser(*user)
+			}
+		case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
+			gatewayClient, err := g.gatewaySelector.Next()
+			if err != nil {
+				g.logger.Error().Err(err).Msg("could not select next gateway client")
+				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+			}
+			req := group.GetGroupRequest{
+				GroupId: s.Grantee.GetGroupId(),
+			}
+			res, err := gatewayClient.GetGroup(ctx, &req)
+			if err != nil {
+				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+			}
+			grp := libregraph.NewIdentityWithDefaults()
+			grp.SetId(s.Grantee.GetGroupId().GetOpaqueId())
+			switch res.Status.Code {
+			case rpc.Code_CODE_OK:
+				cs3Group := res.GetGroup()
+				grp.SetDisplayName(cs3Group.GetDisplayName())
+				grantedTo.SetGroup(*grp)
+			case rpc.Code_CODE_NOT_FOUND:
+				g.logger.Warn().Str("groupid", s.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id")
+				// Group does not seem to exist anymore, don't add a permission for this
+				continue
+			default:
+				return driveItems, errorcode.New(errorcode.GeneralException, res.Status.Message)
+			}
+		}
+
+		// set expiration date
+		if s.GetExpiration() != nil {
+			perm.SetExpirationDateTime(cs3TimestampToTime(s.GetExpiration()))
+		}
+
+		perm.SetGrantedToV2(grantedTo)
+		item.Permissions = append(item.Permissions, perm)
+		driveItems[resIDStr] = item
+	}
+	return driveItems, nil
+}
+
+func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
+	for _, s := range shares {
+		g.logger.Debug().Interface("CS3 PublicShare", s).Msg("Got Share")
+		resIDStr := storagespace.FormatResourceID(*s.ResourceId)
+		item, ok := driveItems[resIDStr]
+		if !ok {
+			itemptr, err := g.getDriveItem(ctx, storageprovider.Reference{ResourceId: s.ResourceId})
+			if err != nil {
+				g.logger.Debug().Err(err).Interface("Share", s.ResourceId).Msg("could not stat share, skipping")
+				continue
+			}
+			item = *itemptr
+		}
+		perm := libregraph.Permission{}
+		perm.SetRoles([]string{})
+		perm.SetId(s.Id.OpaqueId)
+		link := libregraph.SharingLink{}
+		webURL, err := url.Parse(g.config.Spaces.WebDavBase)
+		if err != nil {
+			g.logger.Error().
+				Err(err).
+				Str("url", g.config.Spaces.WebDavBase).
+				Msg("failed to parse webURL base url")
+			return driveItems, err
+		}
+
+		webURL.Path = path.Join(webURL.Path, "s", s.GetToken())
+		link.SetWebUrl(webURL.String())
+		perm.SetLink(link)
+		// set expiration date
+		if s.GetExpiration() != nil {
+			perm.SetExpirationDateTime(cs3TimestampToTime(s.GetExpiration()))
+		}
+
+		item.Permissions = append(item.Permissions, perm)
+		driveItems[resIDStr] = item
+	}
+
+	return driveItems, nil
+}
+
+func cs3StatusToErrCode(code rpc.Code) (errcode errorcode.ErrorCode) {
+	switch code {
+	case rpc.Code_CODE_UNAUTHENTICATED:
+		errcode = errorcode.Unauthenticated
+	case rpc.Code_CODE_PERMISSION_DENIED:
+		errcode = errorcode.AccessDenied
+	case rpc.Code_CODE_NOT_FOUND:
+		errcode = errorcode.ItemNotFound
+	default:
+		errcode = errorcode.GeneralException
+	}
+	return errcode
+}
diff --git a/services/ocm/README.md b/services/ocm/README.md
index 4340703618a..30b72789600 100644
--- a/services/ocm/README.md
+++ b/services/ocm/README.md
@@ -1,3 +1,64 @@
 # OCM
 
-The `ocm` service provides federated sharing functionality based on [sciencemesh](https://sciencemesh.io/) and [ocm](https://github.com/cs3org/OCM-API).
\ No newline at end of file
+The `ocm` service provides federated sharing functionality based on the [sciencemesh](https://sciencemesh.io/) and [ocm](https://github.com/cs3org/OCM-API) HTTP APIs. Internally the `ocm` service consists of the following services and endpoints:
+
+External HTTP APIs:
+* sciencemesh: serves the API for the invitation workflow
+* ocmd: serves the API for managing federated shares
+
+Internal GRPC APIs:
+* ocmproviderauthorizer: manages the list of trusted providers and verifies requests
+* ocminvitemanager: manages the list and state of invite tokens
+* ocmshareprovider: manages ocm shares on the sharer
+* ocmcore: used for creating federated shares on the receiver side
+* authprovider: authenticates webdav requests using the ocm share tokens
+
+## Trust Between Instances
+
+The `ocm` services implements an invitation workflow which needs to be followed before creating federated shares. Invitations are limited to trusted instances, however.
+
+The list of trusted instances is managed by the `ocmproviderauthorizer` service. The only supported backend currently is `json` which stores the list in a json file on disk.
+
+Example `providers.json` file:
+```
+[
+    {
+        "name": "Example",
+        "full_name": "Example provider",
+        "organization": "Owncloud",
+        "domain": "example.com",
+        "homepage": "https://example.com.com",
+        "services": [
+            {
+                "endpoint": {
+                    "type": {
+                        "name": "OCM",
+                        "description": "example.com Open Cloud Mesh API"
+                    },
+                    "name": "example.com - OCM API",
+                    "path": "https://example.com/ocm/",
+                    "is_monitored": true
+                },
+                "api_version": "0.0.1",
+                "host": "example.com"
+            }
+        ]
+    },
+]
+```
+
+## Invitation Workflow
+
+Before sharing a resource with a remote user this user has to be invited by the sharer.
+
+In order to do so a POST request is sent to the `generate-invite` endpoint of the sciencemesh API. The generated token is passed on to the receiver, who will then use the `accept-invite` endpoint to accept the invitation. As a result remote users will be added to the `ocminvitemanager` on both sides. See [invitation flow](invitation_flow) for the according sequence diagram.
+
+The data backend of the `ocminvitemanager` is configurable. The only supported backend currently is `json` which stores the data in a json file on disk.
+
+## Creating Shares
+
+OCM Shares are currently created using the ocs API, just like regular shares. The difference is the share type, which is 6 (ShareTypeFederatedCloudShare) in this case, and a few additional parameters required for identifying the remote user.
+
+See [Create share flow](create_share_flow) for the according sequence diagram.
+
+The data backends of the `ocmshareprovider` and `ocmcore` services are configurable. The only supported backend currently is `json` which stores the data in a json file on disk.
diff --git a/services/ocm/pkg/config/config.go b/services/ocm/pkg/config/config.go
index 17e53a2fdbd..f78d0bc6aff 100644
--- a/services/ocm/pkg/config/config.go
+++ b/services/ocm/pkg/config/config.go
@@ -100,7 +100,7 @@ type OCMProviderAuthorizerDrivers struct {
 
 type OCMProviderAuthorizerJSONDriver struct {
 	Providers             string `yaml:"providers" env:"OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE" desc:"Path to the JSON file where ocm invites data will be stored. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/storage."`
-	VerifyRequestHostname bool   `yaml:"verify_request_hostname" env:"OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE"`
+	VerifyRequestHostname bool   `yaml:"verify_request_hostname" env:"OCM_OCM_PROVIDER_AUTHORIZER_VERIFY_REQUEST_HOSTNAME" desc:"Verify the hostname of the incoming request against the hostname of the OCM provider."`
 }
 
 type OCMCore struct {
diff --git a/services/search/README.md b/services/search/README.md
index 739fc3a6771..520d89516d6 100644
--- a/services/search/README.md
+++ b/services/search/README.md
@@ -70,8 +70,7 @@ When the search service can reach Tika, it begins to read out the content on dem
 
 Content extraction and handling the extracted content can be very resource intensive. Content extraction is therefore limited to files with a certain file size. The default limit is 20MB and can be configured using the `SEARCH_CONTENT_EXTRACTION_SIZE_LIMIT` variable.
 
-When extracting the content you can specify whether filler words are ignored or not.
-To keep them, the environment variable `SEARCH_EXTRACTOR_TIKA_CLEAN_STOP_WORDS` must be set to false.
+When extracting content, you can specify whether [stop words](https://en.wikipedia.org/wiki/Stop_word) like `I`, `you`, `the` are ignored or not. Noramlly, these stop words are removed automatically. To keep them, the environment variable `SEARCH_EXTRACTOR_TIKA_CLEAN_STOP_WORDS` must be set to `false`.
 
 When using the Tika container and docker-compose, consider the following:
 
diff --git a/services/search/pkg/content/content.go b/services/search/pkg/content/content.go
index ef334e07064..fccd8062896 100644
--- a/services/search/pkg/content/content.go
+++ b/services/search/pkg/content/content.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 
 	"github.com/bbalet/stopwords"
+	libregraph "github.com/owncloud/libre-graph-api-go"
 )
 
 func init() {
@@ -20,6 +21,7 @@ type Document struct {
 	Mtime    string
 	MimeType string
 	Tags     []string
+	Audio    *libregraph.Audio `json:"audio,omitempty"`
 }
 
 func CleanString(content, langCode string) string {
diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go
index b2953170b48..b169de8fd88 100644
--- a/services/search/pkg/content/tika.go
+++ b/services/search/pkg/content/tika.go
@@ -3,6 +3,7 @@ package content
 import (
 	"context"
 	"fmt"
+	"strconv"
 	"strings"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -10,6 +11,7 @@ import (
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
 	"github.com/google/go-tika/tika"
 
+	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/services/search/pkg/config"
 )
@@ -86,6 +88,68 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document,
 		if content, err := getFirstValue(meta, "X-TIKA:content"); err == nil {
 			doc.Content = strings.TrimSpace(fmt.Sprintf("%s %s", doc.Content, content))
 		}
+
+		if contentType, err := getFirstValue(meta, "Content-Type"); err == nil && strings.HasPrefix(contentType, "audio/") {
+			audio := libregraph.Audio{}
+
+			if v, err := getFirstValue(meta, "xmpDM:album"); err == nil {
+				audio.SetAlbum(v)
+			}
+
+			if v, err := getFirstValue(meta, "xmpDM:albumArtist"); err == nil {
+				audio.SetAlbumArtist(v)
+			}
+
+			if v, err := getFirstValue(meta, "xmpDM:artist"); err == nil {
+				audio.SetArtist(v)
+			}
+
+			// TODO: audio.Bitrate: not provided by tika
+			// TODO: audio.Composers: not provided by tika
+			// TODO: audio.Copyright: not provided by tika for audio files?
+
+			if v, err := getFirstValue(meta, "xmpDM:discNumber"); err == nil {
+				if i, err := strconv.ParseInt(v, 10, 32); err == nil {
+					audio.SetDisc(int32(i))
+				}
+
+			}
+
+			//  TODO: audio.DiscCount: not provided by tika
+
+			if v, err := getFirstValue(meta, "xmpDM:duration"); err == nil {
+				if i, err := strconv.ParseInt(v, 10, 64); err == nil {
+					audio.SetDuration(i * 1000)
+				}
+			}
+
+			if v, err := getFirstValue(meta, "xmpDM:genre"); err == nil {
+				audio.SetGenre(v)
+			}
+
+			// TODO: audio.HasDrm: not provided by tika
+			// TODO: audio.IsVariableBitrate: not provided by tika
+
+			if v, err := getFirstValue(meta, "dc:title"); err == nil {
+				audio.SetTitle(v)
+			}
+
+			if v, err := getFirstValue(meta, "xmpDM:trackNumber"); err == nil {
+				if i, err := strconv.ParseInt(v, 10, 32); err == nil {
+					audio.SetTrack(int32(i))
+				}
+			}
+
+			// TODO: audio.TrackCount: not provided by tika
+
+			if v, err := getFirstValue(meta, "xmpDM:releaseDate"); err == nil {
+				if i, err := strconv.ParseInt(v, 10, 32); err == nil {
+					audio.SetYear(int32(i))
+				}
+			}
+
+			doc.Audio = &audio
+		}
 	}
 
 	if langCode, _ := t.tika.LanguageString(ctx, doc.Content); langCode != "" && t.CleanStopWords {
diff --git a/services/search/pkg/engine/bleve.go b/services/search/pkg/engine/bleve.go
index c4ad66c6d9d..8e1b6efedbb 100644
--- a/services/search/pkg/engine/bleve.go
+++ b/services/search/pkg/engine/bleve.go
@@ -6,6 +6,7 @@ import (
 	"math"
 	"path"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"time"
 
@@ -24,6 +25,7 @@ import (
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
 
+	libregraph "github.com/owncloud/libre-graph-api-go"
 	searchMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
 	searchService "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
 	"github.com/owncloud/ocis/v2/services/search/pkg/content"
@@ -208,6 +210,7 @@ func (b *Bleve) Search(ctx context.Context, sir *searchService.SearchIndexReques
 				Deleted:    getFieldValue[bool](hit.Fields, "Deleted"),
 				Tags:       getFieldSliceValue[string](hit.Fields, "Tags"),
 				Highlights: getFragmentValue(hit.Fragments, "Content", 0),
+				Audio:      getAudioValue[searchMessage.Audio](hit.Fields),
 			},
 		}
 
@@ -325,10 +328,60 @@ func (b *Bleve) getResource(id string) (*Resource, error) {
 			MimeType: getFieldValue[string](fields, "MimeType"),
 			Content:  getFieldValue[string](fields, "Content"),
 			Tags:     getFieldSliceValue[string](fields, "Tags"),
+			Audio:    getAudioValue[libregraph.Audio](fields),
 		},
 	}, nil
 }
 
+func newPointerOfType[T any]() *T {
+	t := reflect.TypeOf((*T)(nil)).Elem()
+	ptr := reflect.New(t).Interface()
+	return ptr.(*T)
+}
+
+func getFieldName(structField reflect.StructField) string {
+	tag := structField.Tag.Get("json")
+	if tag == "" {
+		return structField.Name
+	}
+
+	return strings.Split(tag, ",")[0]
+}
+
+func unmarshalInterfaceMap(out any, flatMap map[string]interface{}, prefix string) bool {
+	nonEmpty := false
+	obj := reflect.ValueOf(out).Elem()
+	for i := 0; i < obj.NumField(); i++ {
+		field := obj.Field(i)
+		structField := obj.Type().Field(i)
+		mapKey := prefix + getFieldName(structField)
+
+		if value, ok := flatMap[mapKey]; ok {
+			if field.Kind() == reflect.Ptr {
+				alloc := reflect.New(field.Type().Elem())
+				alloc.Elem().Set(reflect.ValueOf(value).Convert(field.Type().Elem()))
+				field.Set(alloc)
+				nonEmpty = true
+			}
+		}
+	}
+
+	return nonEmpty
+}
+
+func getAudioValue[T any](fields map[string]interface{}) *T {
+	if !strings.HasPrefix(getFieldValue[string](fields, "MimeType"), "audio/") {
+		return nil
+	}
+
+	var audio = newPointerOfType[T]()
+	if ok := unmarshalInterfaceMap(audio, fields, "audio."); ok {
+		return audio
+	}
+
+	return nil
+}
+
 func (b *Bleve) updateEntity(id string, mutateFunc func(r *Resource)) (*Resource, error) {
 	it, err := b.getResource(id)
 	if err != nil {
diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go
index 8b8ee1f0c66..199b3df31e3 100644
--- a/services/search/pkg/search/service.go
+++ b/services/search/pkg/search/service.go
@@ -5,11 +5,13 @@ import (
 	"fmt"
 	"path/filepath"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
@@ -22,6 +24,7 @@ import (
 	"golang.org/x/sync/errgroup"
 	"google.golang.org/protobuf/types/known/fieldmaskpb"
 
+	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
 	searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
@@ -487,6 +490,70 @@ func (s *Service) UpsertItem(ref *provider.Reference, uID *user.UserId) {
 	} else {
 		logDocCount(s.engine, s.logger)
 	}
+
+	// determine if metadata needs to be stored in storage as well
+	metadata := map[string]string{}
+	addAudioMetadata(metadata, doc.Audio)
+	if len(metadata) == 0 {
+		return
+	}
+
+	s.logger.Trace().Str("name", doc.Name).Interface("metadata", metadata).Msg("Storing metadata")
+
+	gatewayClient, err := s.gatewaySelector.Next()
+	if err != nil {
+		s.logger.Error().Err(err).Msg("could not retrieve client to store metadata")
+		return
+	}
+
+	resp, err := gatewayClient.SetArbitraryMetadata(ctx, &provider.SetArbitraryMetadataRequest{
+		Ref: ref,
+		ArbitraryMetadata: &provider.ArbitraryMetadata{
+			Metadata: metadata,
+		},
+	})
+	if err != nil || resp.GetStatus().GetCode() != rpc.Code_CODE_OK {
+		s.logger.Error().Err(err).Int32("status", int32(resp.GetStatus().GetCode())).Msg("error storing metadata")
+		return
+	}
+}
+
+func addAudioMetadata(metadata map[string]string, audio *libregraph.Audio) {
+	if audio == nil {
+		return
+	}
+	marshalToStringMap(audio, metadata, "libre.graph.audio.")
+}
+
+func marshalToStringMap[T libregraph.MappedNullable](source T, target map[string]string, prefix string) {
+	// ToMap never returns a non-nil error ...
+	m, _ := source.ToMap()
+
+	for k, v := range m {
+		if v == nil {
+			continue
+		}
+		target[prefix+k] = valueToString(v)
+	}
+}
+
+func valueToString(value interface{}) string {
+	if value == nil {
+		return ""
+	}
+
+	switch v := value.(type) {
+	case *string:
+		return *v
+	case *int32:
+		return strconv.FormatInt(int64(*v), 10)
+	case *int64:
+		return strconv.FormatInt(*v, 10)
+	case *bool:
+		return strconv.FormatBool(*v)
+	default:
+		return fmt.Sprintf("%v", v)
+	}
 }
 
 // RestoreItem makes the item available again.
diff --git a/services/thumbnails/pkg/preprocessor/preprocessor.go b/services/thumbnails/pkg/preprocessor/preprocessor.go
index 89d653cf79f..2f6346ec74e 100644
--- a/services/thumbnails/pkg/preprocessor/preprocessor.go
+++ b/services/thumbnails/pkg/preprocessor/preprocessor.go
@@ -2,6 +2,7 @@ package preprocessor
 
 import (
 	"bufio"
+	"bytes"
 	"image"
 	"image/draw"
 	"image/gif"
@@ -15,6 +16,8 @@ import (
 	"golang.org/x/image/font"
 	"golang.org/x/image/font/opentype"
 	"golang.org/x/image/math/fixed"
+
+	"github.com/dhowden/tag"
 )
 
 type FileConverter interface {
@@ -41,6 +44,31 @@ func (i GifDecoder) Convert(r io.Reader) (interface{}, error) {
 	return img, nil
 }
 
+type AudioDecoder struct{}
+
+func (i AudioDecoder) Convert(r io.Reader) (interface{}, error) {
+	b, err := io.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	m, err := tag.ReadFrom(bytes.NewReader(b))
+	if err != nil {
+		return nil, err
+	}
+
+	picture := m.Picture()
+	if picture == nil {
+		return nil, errors.New(`could not extract image from audio file`)
+	}
+
+	converter := ForType(picture.MIMEType, nil)
+	if converter == nil {
+		return nil, errors.New(`could not find converter for image extraced from audio file`)
+	}
+
+	return converter.Convert(bytes.NewReader(picture.Data))
+}
+
 type TxtToImageConverter struct {
 	fontLoader *FontLoader
 }
@@ -197,6 +225,12 @@ func ForType(mimeType string, opts map[string]interface{}) FileConverter {
 		}
 	case "image/gif":
 		return GifDecoder{}
+	case "audio/flac":
+		fallthrough
+	case "audio/mpeg":
+		fallthrough
+	case "audio/ogg":
+		return AudioDecoder{}
 	default:
 		return ImageDecoder{}
 	}
diff --git a/services/thumbnails/pkg/thumbnail/thumbnail.go b/services/thumbnails/pkg/thumbnail/thumbnail.go
index b3de987709d..9a839025efa 100644
--- a/services/thumbnails/pkg/thumbnail/thumbnail.go
+++ b/services/thumbnails/pkg/thumbnail/thumbnail.go
@@ -21,6 +21,9 @@ var (
 		"image/x-ms-bmp": {},
 		"image/tiff":     {},
 		"text/plain":     {},
+		"audio/flac":     {},
+		"audio/mpeg":     {},
+		"audio/ogg":      {},
 	}
 )
 
diff --git a/services/userlog/pkg/service/l10n/locale/de/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/de/LC_MESSAGES/userlog.po
index bfdda0582f4..68fabebd6c4 100644
--- a/services/userlog/pkg/service/l10n/locale/de/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/de/LC_MESSAGES/userlog.po
@@ -14,7 +14,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-10-16 01:14+0000\n"
+"POT-Creation-Date: 2023-11-06 00:04+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Michael Barz <mbarz@owncloud.com>, 2023\n"
 "Language-Team: German (https://app.transifex.com/owncloud-org/teams/6149/de/)\n"
diff --git a/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po
index 3602f22869a..12be4a9bb3d 100644
--- a/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/de_DE/LC_MESSAGES/userlog.po
@@ -13,7 +13,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-10-16 01:14+0000\n"
+"POT-Creation-Date: 2023-11-06 00:04+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Jürgen Weigert <jnweiger@gmail.com>, 2023\n"
 "Language-Team: German (Germany) (https://app.transifex.com/owncloud-org/teams/6149/de_DE/)\n"
diff --git a/vendor/github.com/dhowden/tag/.editorconfig b/vendor/github.com/dhowden/tag/.editorconfig
new file mode 100644
index 00000000000..57515d0b7f7
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*.go]
+indent_style = tab
+indent_size = 3
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*]
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/vendor/github.com/dhowden/tag/.travis.yml b/vendor/github.com/dhowden/tag/.travis.yml
new file mode 100644
index 00000000000..3a81bf933c0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.travis.yml
@@ -0,0 +1,5 @@
+language: go
+
+go:
+  - 1.7
+  - tip
\ No newline at end of file
diff --git a/vendor/github.com/dhowden/tag/LICENSE b/vendor/github.com/dhowden/tag/LICENSE
new file mode 100644
index 00000000000..dfd760c4fd5
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2015, David Howden
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+  Redistributions in binary form must reproduce the above copyright notice, this
+  list of conditions and the following disclaimer in the documentation and/or
+  other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/dhowden/tag/README.md b/vendor/github.com/dhowden/tag/README.md
new file mode 100644
index 00000000000..dbc05e0c2f0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/README.md
@@ -0,0 +1,71 @@
+# MP3/MP4/OGG/FLAC metadata parsing library
+[![GoDoc](https://pkg.go.dev/badge/github.com/dhowden/tag)](https://pkg.go.dev/github.com/dhowden/tag)
+
+This package provides MP3 (ID3v1,2.{2,3,4}) and MP4 (ACC, M4A, ALAC), OGG and FLAC metadata detection, parsing and artwork extraction.
+
+Detect and parse tag metadata from an `io.ReadSeeker` (i.e. an `*os.File`):
+
+```go
+m, err := tag.ReadFrom(f)
+if err != nil {
+	log.Fatal(err)
+}
+log.Print(m.Format()) // The detected format.
+log.Print(m.Title())  // The title of the track (see Metadata interface for more details).
+```
+
+Parsed metadata is exported via a single interface (giving a consistent API for all supported metadata formats).
+
+```go
+// Metadata is an interface which is used to describe metadata retrieved by this package.
+type Metadata interface {
+	Format() Format
+	FileType() FileType
+
+	Title() string
+	Album() string
+	Artist() string
+	AlbumArtist() string
+	Composer() string
+	Genre() string
+	Year() int
+
+	Track() (int, int) // Number, Total
+	Disc() (int, int) // Number, Total
+
+	Picture() *Picture // Artwork
+	Lyrics() string
+	Comment() string
+
+	Raw() map[string]interface{} // NB: raw tag names are not consistent across formats.
+}
+```
+
+## Audio Data Checksum (SHA1)
+
+This package also provides a metadata-invariant checksum for audio files: only the audio data is used to
+construct the checksum.
+
+[https://pkg.go.dev/github.com/dhowden/tag#Sum](https://pkg.go.dev/github.com/dhowden/tag#Sum)
+
+## Tools
+
+There are simple command-line tools which demonstrate basic tag extraction and summing:
+
+```console
+$ go install github.com/dhowden/tag/cmd/tag@latest
+$ cd $GOPATH/bin
+$ ./tag 11\ High\ Hopes.m4a
+Metadata Format: MP4
+Title: High Hopes
+Album: The Division Bell
+Artist: Pink Floyd
+Composer: Abbey Road Recording Studios/David Gilmour/Polly Samson
+Year: 1994
+Track: 11 of 11
+Disc: 1 of 1
+Picture: Picture{Ext: jpeg, MIMEType: image/jpeg, Type: , Description: , Data.Size: 606109}
+
+$ ./sum 11\ High\ Hopes.m4a
+2ae208c5f00a1f21f5fac9b7f6e0b8e52c06da29
+```
diff --git a/vendor/github.com/dhowden/tag/dsf.go b/vendor/github.com/dhowden/tag/dsf.go
new file mode 100644
index 00000000000..8ca34f21da9
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/dsf.go
@@ -0,0 +1,109 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"errors"
+	"io"
+)
+
+// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+// samples: http://www.2l.no/hires/index.html
+func ReadDSFTags(r io.ReadSeeker) (Metadata, error) {
+	dsd, err := readString(r, 4)
+	if err != nil {
+		return nil, err
+	}
+	if dsd != "DSD " {
+		return nil, errors.New("expected 'DSD '")
+	}
+
+	_, err = r.Seek(int64(16), io.SeekCurrent)
+	if err != nil {
+		return nil, err
+	}
+
+	id3Pointer, err := readUint64LittleEndian(r)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = r.Seek(int64(id3Pointer), io.SeekStart)
+	if err != nil {
+		return nil, err
+	}
+
+	id3, err := ReadID3v2Tags(r)
+	if err != nil {
+		return nil, err
+	}
+
+	return metadataDSF{id3}, nil
+}
+
+type metadataDSF struct {
+	id3 Metadata
+}
+
+func (m metadataDSF) Format() Format {
+	return m.id3.Format()
+}
+
+func (m metadataDSF) FileType() FileType {
+	return DSF
+}
+
+func (m metadataDSF) Title() string {
+	return m.id3.Title()
+}
+
+func (m metadataDSF) Album() string {
+	return m.id3.Album()
+}
+
+func (m metadataDSF) Artist() string {
+	return m.id3.Artist()
+}
+
+func (m metadataDSF) AlbumArtist() string {
+	return m.id3.AlbumArtist()
+}
+
+func (m metadataDSF) Composer() string {
+	return m.id3.Composer()
+}
+
+func (m metadataDSF) Year() int {
+	return m.id3.Year()
+}
+
+func (m metadataDSF) Genre() string {
+	return m.id3.Genre()
+}
+
+func (m metadataDSF) Track() (int, int) {
+	return m.id3.Track()
+}
+
+func (m metadataDSF) Disc() (int, int) {
+	return m.id3.Disc()
+}
+
+func (m metadataDSF) Picture() *Picture {
+	return m.id3.Picture()
+}
+
+func (m metadataDSF) Lyrics() string {
+	return m.id3.Lyrics()
+}
+
+func (m metadataDSF) Comment() string {
+	return m.id3.Comment()
+}
+
+func (m metadataDSF) Raw() map[string]interface{} {
+	return m.id3.Raw()
+}
diff --git a/vendor/github.com/dhowden/tag/flac.go b/vendor/github.com/dhowden/tag/flac.go
new file mode 100644
index 00000000000..c3704673f00
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/flac.go
@@ -0,0 +1,89 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"errors"
+	"io"
+)
+
+// blockType is a type which represents an enumeration of valid FLAC blocks
+type blockType byte
+
+// FLAC block types.
+const (
+	// Stream Info Block           0
+	// Padding Block               1
+	// Application Block           2
+	// Seektable Block             3
+	// Cue Sheet Block             5
+	vorbisCommentBlock blockType = 4
+	pictureBlock       blockType = 6
+)
+
+// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
+	flac, err := readString(r, 4)
+	if err != nil {
+		return nil, err
+	}
+	if flac != "fLaC" {
+		return nil, errors.New("expected 'fLaC'")
+	}
+
+	m := &metadataFLAC{
+		newMetadataVorbis(),
+	}
+
+	for {
+		last, err := m.readFLACMetadataBlock(r)
+		if err != nil {
+			return nil, err
+		}
+
+		if last {
+			break
+		}
+	}
+	return m, nil
+}
+
+type metadataFLAC struct {
+	*metadataVorbis
+}
+
+func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+	blockHeader, err := readBytes(r, 1)
+	if err != nil {
+		return
+	}
+
+	if getBit(blockHeader[0], 7) {
+		blockHeader[0] ^= (1 << 7)
+		last = true
+	}
+
+	blockLen, err := readInt(r, 3)
+	if err != nil {
+		return
+	}
+
+	switch blockType(blockHeader[0]) {
+	case vorbisCommentBlock:
+		err = m.readVorbisComment(r)
+
+	case pictureBlock:
+		err = m.readPictureBlock(r)
+
+	default:
+		_, err = r.Seek(int64(blockLen), io.SeekCurrent)
+	}
+	return
+}
+
+func (m *metadataFLAC) FileType() FileType {
+	return FLAC
+}
diff --git a/vendor/github.com/dhowden/tag/id.go b/vendor/github.com/dhowden/tag/id.go
new file mode 100644
index 00000000000..24103568dba
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id.go
@@ -0,0 +1,81 @@
+package tag
+
+import (
+	"fmt"
+	"io"
+)
+
+// Identify identifies the format and file type of the data in the ReadSeeker.
+func Identify(r io.ReadSeeker) (format Format, fileType FileType, err error) {
+	b, err := readBytes(r, 11)
+	if err != nil {
+		return
+	}
+
+	_, err = r.Seek(-11, io.SeekCurrent)
+	if err != nil {
+		err = fmt.Errorf("could not seek back to original position: %v", err)
+		return
+	}
+
+	switch {
+	case string(b[0:4]) == "fLaC":
+		return VORBIS, FLAC, nil
+
+	case string(b[0:4]) == "OggS":
+		return VORBIS, OGG, nil
+
+	case string(b[4:8]) == "ftyp":
+		b = b[8:11]
+		fileType = UnknownFileType
+		switch string(b) {
+		case "M4A":
+			fileType = M4A
+
+		case "M4B":
+			fileType = M4B
+
+		case "M4P":
+			fileType = M4P
+		}
+		return MP4, fileType, nil
+
+	case string(b[0:3]) == "ID3":
+		b := b[3:]
+		switch uint(b[0]) {
+		case 2:
+			format = ID3v2_2
+		case 3:
+			format = ID3v2_3
+		case 4:
+			format = ID3v2_4
+		case 0, 1:
+			fallthrough
+		default:
+			err = fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
+			return
+		}
+		return format, MP3, nil
+	}
+
+	n, err := r.Seek(-128, io.SeekEnd)
+	if err != nil {
+		return
+	}
+
+	tag, err := readString(r, 3)
+	if err != nil {
+		return
+	}
+
+	_, err = r.Seek(-n, io.SeekCurrent)
+	if err != nil {
+		return
+	}
+
+	if tag != "TAG" {
+		err = ErrNoTagsFound
+		return
+	}
+	return ID3v1, MP3, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v1.go b/vendor/github.com/dhowden/tag/id3v1.go
new file mode 100644
index 00000000000..0953f0beedc
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v1.go
@@ -0,0 +1,144 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"errors"
+	"io"
+	"strconv"
+	"strings"
+)
+
+// id3v1Genres is a list of genres as given in the ID3v1 specification.
+var id3v1Genres = [...]string{
+	"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+	"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+	"Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+	"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
+	"Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
+	"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
+	"Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
+	"Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+	"Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
+	"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
+	"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
+	"Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
+	"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
+	"Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
+	"National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+	"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
+	"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
+	"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
+	"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+	"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+	"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
+	"Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall",
+}
+
+// ErrNotID3v1 is an error which is returned when no ID3v1 header is found.
+var ErrNotID3v1 = errors.New("invalid ID3v1 header")
+
+// ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker.  Returns ErrNotID3v1
+// if there are no ID3v1 tags, otherwise non-nil error if there was a problem.
+func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) {
+	_, err := r.Seek(-128, io.SeekEnd)
+	if err != nil {
+		return nil, err
+	}
+
+	if tag, err := readString(r, 3); err != nil {
+		return nil, err
+	} else if tag != "TAG" {
+		return nil, ErrNotID3v1
+	}
+
+	title, err := readString(r, 30)
+	if err != nil {
+		return nil, err
+	}
+
+	artist, err := readString(r, 30)
+	if err != nil {
+		return nil, err
+	}
+
+	album, err := readString(r, 30)
+	if err != nil {
+		return nil, err
+	}
+
+	year, err := readString(r, 4)
+	if err != nil {
+		return nil, err
+	}
+
+	commentBytes, err := readBytes(r, 30)
+	if err != nil {
+		return nil, err
+	}
+
+	var comment string
+	var track int
+	if commentBytes[28] == 0 {
+		comment = trimString(string(commentBytes[:28]))
+		track = int(commentBytes[29])
+	} else {
+		comment = trimString(string(commentBytes))
+	}
+
+	var genre string
+	genreID, err := readBytes(r, 1)
+	if err != nil {
+		return nil, err
+	}
+	if int(genreID[0]) < len(id3v1Genres) {
+		genre = id3v1Genres[int(genreID[0])]
+	}
+
+	m := make(map[string]interface{})
+	m["title"] = trimString(title)
+	m["artist"] = trimString(artist)
+	m["album"] = trimString(album)
+	m["year"] = trimString(year)
+	m["comment"] = trimString(comment)
+	m["track"] = track
+	m["genre"] = genre
+
+	return metadataID3v1(m), nil
+}
+
+func trimString(x string) string {
+	return strings.TrimSpace(strings.Trim(x, "\x00"))
+}
+
+// metadataID3v1 is the implementation of Metadata used for ID3v1 tags.
+type metadataID3v1 map[string]interface{}
+
+func (metadataID3v1) Format() Format                { return ID3v1 }
+func (metadataID3v1) FileType() FileType            { return MP3 }
+func (m metadataID3v1) Raw() map[string]interface{} { return m }
+
+func (m metadataID3v1) Title() string  { return m["title"].(string) }
+func (m metadataID3v1) Album() string  { return m["album"].(string) }
+func (m metadataID3v1) Artist() string { return m["artist"].(string) }
+func (m metadataID3v1) Genre() string  { return m["genre"].(string) }
+
+func (m metadataID3v1) Year() int {
+	y := m["year"].(string)
+	n, err := strconv.Atoi(y)
+	if err != nil {
+		return 0
+	}
+	return n
+}
+
+func (m metadataID3v1) Track() (int, int) { return m["track"].(int), 0 }
+
+func (m metadataID3v1) AlbumArtist() string { return "" }
+func (m metadataID3v1) Composer() string    { return "" }
+func (metadataID3v1) Disc() (int, int)      { return 0, 0 }
+func (m metadataID3v1) Picture() *Picture   { return nil }
+func (m metadataID3v1) Lyrics() string      { return "" }
+func (m metadataID3v1) Comment() string     { return m["comment"].(string) }
diff --git a/vendor/github.com/dhowden/tag/id3v2.go b/vendor/github.com/dhowden/tag/id3v2.go
new file mode 100644
index 00000000000..cb6c662c7ab
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2.go
@@ -0,0 +1,458 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+var id3v2Genres = [...]string{
+	"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+	"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+	"Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+	"Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
+	"Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
+	"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
+	"Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
+	"Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+	"Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
+	"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
+	"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
+	"Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer",
+	"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
+	"Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
+	"National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+	"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
+	"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
+	"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
+	"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+	"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+	"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
+	"Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
+	"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
+	"Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap",
+	"Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
+	"Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
+	"Synthpop",
+}
+
+// id3v2Header is a type which represents an ID3v2 tag header.
+type id3v2Header struct {
+	Version           Format
+	Unsynchronisation bool
+	ExtendedHeader    bool
+	Experimental      bool
+	Size              uint
+}
+
+// readID3v2Header reads the ID3v2 header from the given io.Reader.
+// offset it number of bytes of header that was read
+func readID3v2Header(r io.Reader) (h *id3v2Header, offset uint, err error) {
+	offset = 10
+	b, err := readBytes(r, offset)
+	if err != nil {
+		return nil, 0, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err)
+	}
+
+	if string(b[0:3]) != "ID3" {
+		return nil, 0, fmt.Errorf("expected to read \"ID3\"")
+	}
+
+	b = b[3:]
+	var vers Format
+	switch uint(b[0]) {
+	case 2:
+		vers = ID3v2_2
+	case 3:
+		vers = ID3v2_3
+	case 4:
+		vers = ID3v2_4
+	case 0, 1:
+		fallthrough
+	default:
+		return nil, 0, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
+	}
+
+	// NB: We ignore b[1] (the revision) as we don't currently rely on it.
+	h = &id3v2Header{
+		Version:           vers,
+		Unsynchronisation: getBit(b[2], 7),
+		ExtendedHeader:    getBit(b[2], 6),
+		Experimental:      getBit(b[2], 5),
+		Size:              uint(get7BitChunkedInt(b[3:7])),
+	}
+
+	if h.ExtendedHeader {
+		switch vers {
+		case ID3v2_3:
+			b, err := readBytes(r, 4)
+			if err != nil {
+				return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v23 extended header len): %v", err)
+			}
+			// skip header, size is excluding len bytes
+			extendedHeaderSize := uint(getInt(b))
+			_, err = readBytes(r, extendedHeaderSize)
+			if err != nil {
+				return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v23 skip extended header): %v", extendedHeaderSize, err)
+			}
+			offset += extendedHeaderSize
+		case ID3v2_4:
+			b, err := readBytes(r, 4)
+			if err != nil {
+				return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v24 extended header len): %v", err)
+			}
+			// skip header, size is synchsafe int including len bytes
+			extendedHeaderSize := uint(get7BitChunkedInt(b)) - 4
+			_, err = readBytes(r, extendedHeaderSize)
+			if err != nil {
+				return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v24 skip extended header): %v", extendedHeaderSize, err)
+			}
+			offset += extendedHeaderSize
+		default:
+			// nop, only 2.3 and 2.4 should have extended header
+		}
+	}
+
+	return h, offset, nil
+}
+
+// id3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame.
+type id3v2FrameFlags struct {
+	// Message (ID3 2.3.0 and 2.4.0)
+	TagAlterPreservation  bool
+	FileAlterPreservation bool
+	ReadOnly              bool
+
+	// Format (ID3 2.3.0 and 2.4.0)
+	Compression   bool
+	Encryption    bool
+	GroupIdentity bool
+	// ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1)
+	Unsynchronisation   bool
+	DataLengthIndicator bool
+}
+
+func readID3v23FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
+	b, err := readBytes(r, 2)
+	if err != nil {
+		return nil, err
+	}
+
+	msg := b[0]
+	fmt := b[1]
+
+	return &id3v2FrameFlags{
+		TagAlterPreservation:  getBit(msg, 7),
+		FileAlterPreservation: getBit(msg, 6),
+		ReadOnly:              getBit(msg, 5),
+		Compression:           getBit(fmt, 7),
+		Encryption:            getBit(fmt, 6),
+		GroupIdentity:         getBit(fmt, 5),
+	}, nil
+}
+
+func readID3v24FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
+	b, err := readBytes(r, 2)
+	if err != nil {
+		return nil, err
+	}
+
+	msg := b[0]
+	fmt := b[1]
+
+	return &id3v2FrameFlags{
+		TagAlterPreservation:  getBit(msg, 6),
+		FileAlterPreservation: getBit(msg, 5),
+		ReadOnly:              getBit(msg, 4),
+		GroupIdentity:         getBit(fmt, 6),
+		Compression:           getBit(fmt, 3),
+		Encryption:            getBit(fmt, 2),
+		Unsynchronisation:     getBit(fmt, 1),
+		DataLengthIndicator:   getBit(fmt, 0),
+	}, nil
+
+}
+
+func readID3v2_2FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
+	name, err = readString(r, 3)
+	if err != nil {
+		return
+	}
+	size, err = readUint(r, 3)
+	if err != nil {
+		return
+	}
+	headerSize = 6
+	return
+}
+
+func readID3v2_3FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
+	name, err = readString(r, 4)
+	if err != nil {
+		return
+	}
+	size, err = readUint(r, 4)
+	if err != nil {
+		return
+	}
+	headerSize = 8
+	return
+}
+
+func readID3v2_4FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
+	name, err = readString(r, 4)
+	if err != nil {
+		return
+	}
+	size, err = read7BitChunkedUint(r, 4)
+	if err != nil {
+		return
+	}
+	headerSize = 8
+	return
+}
+
+// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header.
+func readID3v2Frames(r io.Reader, offset uint, h *id3v2Header) (map[string]interface{}, error) {
+	result := make(map[string]interface{})
+
+	for offset < h.Size {
+		var err error
+		var name string
+		var size, headerSize uint
+		var flags *id3v2FrameFlags
+
+		switch h.Version {
+		case ID3v2_2:
+			name, size, headerSize, err = readID3v2_2FrameHeader(r)
+
+		case ID3v2_3:
+			name, size, headerSize, err = readID3v2_3FrameHeader(r)
+			if err != nil {
+				return nil, err
+			}
+			flags, err = readID3v23FrameFlags(r)
+			headerSize += 2
+
+		case ID3v2_4:
+			name, size, headerSize, err = readID3v2_4FrameHeader(r)
+			if err != nil {
+				return nil, err
+			}
+			flags, err = readID3v24FrameFlags(r)
+			headerSize += 2
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		// FIXME: Do we still need this?
+		// if size=0, we certainly are in a padding zone. ignore the rest of
+		// the tags
+		if size == 0 {
+			break
+		}
+
+		offset += headerSize + size
+
+		// Avoid corrupted padding (see http://id3.org/Compliance%20Issues).
+		if !validID3Frame(h.Version, name) && offset > h.Size {
+			break
+		}
+
+		if flags != nil {
+			if flags.Compression {
+				switch h.Version {
+				case ID3v2_3:
+					// No data length indicator defined.
+					if _, err := read7BitChunkedUint(r, 4); err != nil { // read 4
+						return nil, err
+					}
+					size -= 4
+
+				case ID3v2_4:
+					// Must have a data length indicator (to give the size) if compression is enabled.
+					if !flags.DataLengthIndicator {
+						return nil, errors.New("compression without data length indicator")
+					}
+
+				default:
+					return nil, fmt.Errorf("unsupported compression flag used in %v", h.Version)
+				}
+			}
+
+			if flags.DataLengthIndicator {
+				if h.Version == ID3v2_3 {
+					return nil, fmt.Errorf("data length indicator set but not defined for %v", ID3v2_3)
+				}
+
+				size, err = read7BitChunkedUint(r, 4)
+				if err != nil { // read 4
+					return nil, err
+				}
+			}
+
+			if flags.Encryption {
+				_, err = readBytes(r, 1) // read 1 byte of encryption method
+				if err != nil {
+					return nil, err
+				}
+				size--
+			}
+		}
+
+		b, err := readBytes(r, size)
+		if err != nil {
+			return nil, err
+		}
+
+		// There can be multiple tag with the same name. Append a number to the
+		// name if there is more than one.
+		rawName := name
+		if _, ok := result[rawName]; ok {
+			for i := 0; ok; i++ {
+				rawName = name + "_" + strconv.Itoa(i)
+				_, ok = result[rawName]
+			}
+		}
+
+		switch {
+		case name == "TXXX" || name == "TXX":
+			t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = t
+
+		case name[0] == 'T':
+			txt, err := readTFrame(b)
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = txt
+
+		case name == "UFID" || name == "UFI":
+			t, err := readUFID(b)
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = t
+
+		case name == "WXXX" || name == "WXX":
+			t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = t
+
+		case name[0] == 'W':
+			txt, err := readWFrame(b)
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = txt
+
+		case name == "COMM" || name == "COM" || name == "USLT" || name == "ULT":
+			t, err := readTextWithDescrFrame(b, true, true) // both lang and enc
+			if err != nil {
+				return nil, fmt.Errorf("could not read %q (%q): %v", name, rawName, err)
+			}
+			result[rawName] = t
+
+		case name == "APIC":
+			p, err := readAPICFrame(b)
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = p
+
+		case name == "PIC":
+			p, err := readPICFrame(b)
+			if err != nil {
+				return nil, err
+			}
+			result[rawName] = p
+
+		default:
+			result[rawName] = b
+		}
+	}
+	return result, nil
+}
+
+type unsynchroniser struct {
+	io.Reader
+	ff bool
+}
+
+// filter io.Reader which skip the Unsynchronisation bytes
+func (r *unsynchroniser) Read(p []byte) (int, error) {
+	b := make([]byte, 1)
+	i := 0
+	for i < len(p) {
+		if n, err := r.Reader.Read(b); err != nil || n == 0 {
+			return i, err
+		}
+		if r.ff && b[0] == 0x00 {
+			r.ff = false
+			continue
+		}
+		p[i] = b[0]
+		i++
+		r.ff = (b[0] == 0xFF)
+	}
+	return i, nil
+}
+
+// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
+// non-nil error on failure.
+func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
+	h, offset, err := readID3v2Header(r)
+	if err != nil {
+		return nil, err
+	}
+
+	var ur io.Reader = r
+	if h.Unsynchronisation {
+		ur = &unsynchroniser{Reader: r}
+	}
+
+	f, err := readID3v2Frames(ur, offset, h)
+	if err != nil {
+		return nil, err
+	}
+	return metadataID3v2{header: h, frames: f}, nil
+}
+
+var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`)
+
+//  id3v2genre parse a id3v2 genre tag and expand the numeric genres
+func id3v2genre(genre string) string {
+	c := true
+	for c {
+		orig := genre
+		if match := id3v2genreRe.FindStringSubmatch(genre); len(match) > 0 {
+			if genreID, err := strconv.Atoi(match[2]); err == nil {
+				if genreID < len(id3v2Genres) {
+					genre = id3v2Genres[genreID]
+					if match[1] != "" {
+						genre = strings.TrimSpace(match[1]) + " " + genre
+					}
+					if match[3] != "" {
+						genre = genre + " " + match[3]
+					}
+				}
+			}
+		}
+		c = (orig != genre)
+	}
+	return strings.Replace(genre, "((", "(", -1)
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2frames.go b/vendor/github.com/dhowden/tag/id3v2frames.go
new file mode 100644
index 00000000000..8397eba8fcb
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2frames.go
@@ -0,0 +1,655 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"strings"
+	"unicode/utf16"
+)
+
+// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding
+// is specified without a corresponding BOM in the data.
+var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian
+
+// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4).
+var id3v22Frames = map[string]string{
+	"BUF": "Recommended buffer size",
+
+	"CNT": "Play counter",
+	"COM": "Comments",
+	"CRA": "Audio encryption",
+	"CRM": "Encrypted meta frame",
+
+	"ETC": "Event timing codes",
+	"EQU": "Equalization",
+
+	"GEO": "General encapsulated object",
+
+	"IPL": "Involved people list",
+
+	"LNK": "Linked information",
+
+	"MCI": "Music CD Identifier",
+	"MLL": "MPEG location lookup table",
+
+	"PIC": "Attached picture",
+	"POP": "Popularimeter",
+
+	"REV": "Reverb",
+	"RVA": "Relative volume adjustment",
+
+	"SLT": "Synchronized lyric/text",
+	"STC": "Synced tempo codes",
+
+	"TAL": "Album/Movie/Show title",
+	"TBP": "BPM (Beats Per Minute)",
+	"TCM": "Composer",
+	"TCO": "Content type",
+	"TCR": "Copyright message",
+	"TDA": "Date",
+	"TDY": "Playlist delay",
+	"TEN": "Encoded by",
+	"TFT": "File type",
+	"TIM": "Time",
+	"TKE": "Initial key",
+	"TLA": "Language(s)",
+	"TLE": "Length",
+	"TMT": "Media type",
+	"TOA": "Original artist(s)/performer(s)",
+	"TOF": "Original filename",
+	"TOL": "Original Lyricist(s)/text writer(s)",
+	"TOR": "Original release year",
+	"TOT": "Original album/Movie/Show title",
+	"TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
+	"TP2": "Band/Orchestra/Accompaniment",
+	"TP3": "Conductor/Performer refinement",
+	"TP4": "Interpreted, remixed, or otherwise modified by",
+	"TPA": "Part of a set",
+	"TPB": "Publisher",
+	"TRC": "ISRC (International Standard Recording Code)",
+	"TRD": "Recording dates",
+	"TRK": "Track number/Position in set",
+	"TSI": "Size",
+	"TSS": "Software/hardware and settings used for encoding",
+	"TT1": "Content group description",
+	"TT2": "Title/Songname/Content description",
+	"TT3": "Subtitle/Description refinement",
+	"TXT": "Lyricist/text writer",
+	"TXX": "User defined text information frame",
+	"TYE": "Year",
+
+	"UFI": "Unique file identifier",
+	"ULT": "Unsychronized lyric/text transcription",
+
+	"WAF": "Official audio file webpage",
+	"WAR": "Official artist/performer webpage",
+	"WAS": "Official audio source webpage",
+	"WCM": "Commercial information",
+	"WCP": "Copyright/Legal information",
+	"WPB": "Publishers official webpage",
+	"WXX": "User defined URL link frame",
+}
+
+// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
+var id3v23Frames = map[string]string{
+	"AENC": "Audio encryption]",
+	"APIC": "Attached picture",
+	"COMM": "Comments",
+	"COMR": "Commercial frame",
+	"ENCR": "Encryption method registration",
+	"EQUA": "Equalization",
+	"ETCO": "Event timing codes",
+	"GEOB": "General encapsulated object",
+	"GRID": "Group identification registration",
+	"IPLS": "Involved people list",
+	"LINK": "Linked information",
+	"MCDI": "Music CD identifier",
+	"MLLT": "MPEG location lookup table",
+	"OWNE": "Ownership frame",
+	"PRIV": "Private frame",
+	"PCNT": "Play counter",
+	"POPM": "Popularimeter",
+	"POSS": "Position synchronisation frame",
+	"RBUF": "Recommended buffer size",
+	"RVAD": "Relative volume adjustment",
+	"RVRB": "Reverb",
+	"SYLT": "Synchronized lyric/text",
+	"SYTC": "Synchronized tempo codes",
+	"TALB": "Album/Movie/Show title",
+	"TBPM": "BPM (beats per minute)",
+	"TCMP": "iTunes Compilation Flag",
+	"TCOM": "Composer",
+	"TCON": "Content type",
+	"TCOP": "Copyright message",
+	"TDAT": "Date",
+	"TDLY": "Playlist delay",
+	"TENC": "Encoded by",
+	"TEXT": "Lyricist/Text writer",
+	"TFLT": "File type",
+	"TIME": "Time",
+	"TIT1": "Content group description",
+	"TIT2": "Title/songname/content description",
+	"TIT3": "Subtitle/Description refinement",
+	"TKEY": "Initial key",
+	"TLAN": "Language(s)",
+	"TLEN": "Length",
+	"TMED": "Media type",
+	"TOAL": "Original album/movie/show title",
+	"TOFN": "Original filename",
+	"TOLY": "Original lyricist(s)/text writer(s)",
+	"TOPE": "Original artist(s)/performer(s)",
+	"TORY": "Original release year",
+	"TOWN": "File owner/licensee",
+	"TPE1": "Lead performer(s)/Soloist(s)",
+	"TPE2": "Band/orchestra/accompaniment",
+	"TPE3": "Conductor/performer refinement",
+	"TPE4": "Interpreted, remixed, or otherwise modified by",
+	"TPOS": "Part of a set",
+	"TPUB": "Publisher",
+	"TRCK": "Track number/Position in set",
+	"TRDA": "Recording dates",
+	"TRSN": "Internet radio station name",
+	"TRSO": "Internet radio station owner",
+	"TSIZ": "Size",
+	"TSO2": "iTunes uses this for Album Artist sort order",
+	"TSOC": "iTunes uses this for Composer sort order",
+	"TSRC": "ISRC (international standard recording code)",
+	"TSSE": "Software/Hardware and settings used for encoding",
+	"TYER": "Year",
+	"TXXX": "User defined text information frame",
+	"UFID": "Unique file identifier",
+	"USER": "Terms of use",
+	"USLT": "Unsychronized lyric/text transcription",
+	"WCOM": "Commercial information",
+	"WCOP": "Copyright/Legal information",
+	"WOAF": "Official audio file webpage",
+	"WOAR": "Official artist/performer webpage",
+	"WOAS": "Official audio source webpage",
+	"WORS": "Official internet radio station homepage",
+	"WPAY": "Payment",
+	"WPUB": "Publishers official webpage",
+	"WXXX": "User defined URL link frame",
+}
+
+// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4).
+var id3v24Frames = map[string]string{
+	"AENC": "Audio encryption",
+	"APIC": "Attached picture",
+	"ASPI": "Audio seek point index",
+
+	"COMM": "Comments",
+	"COMR": "Commercial frame",
+
+	"ENCR": "Encryption method registration",
+	"EQU2": "Equalisation (2)",
+	"ETCO": "Event timing codes",
+
+	"GEOB": "General encapsulated object",
+	"GRID": "Group identification registration",
+
+	"LINK": "Linked information",
+
+	"MCDI": "Music CD identifier",
+	"MLLT": "MPEG location lookup table",
+
+	"OWNE": "Ownership frame",
+
+	"PRIV": "Private frame",
+	"PCNT": "Play counter",
+	"POPM": "Popularimeter",
+	"POSS": "Position synchronisation frame",
+
+	"RBUF": "Recommended buffer size",
+	"RVA2": "Relative volume adjustment (2)",
+	"RVRB": "Reverb",
+
+	"SEEK": "Seek frame",
+	"SIGN": "Signature frame",
+	"SYLT": "Synchronised lyric/text",
+	"SYTC": "Synchronised tempo codes",
+
+	"TALB": "Album/Movie/Show title",
+	"TBPM": "BPM (beats per minute)",
+	"TCMP": "iTunes Compilation Flag",
+	"TCOM": "Composer",
+	"TCON": "Content type",
+	"TCOP": "Copyright message",
+	"TDEN": "Encoding time",
+	"TDLY": "Playlist delay",
+	"TDOR": "Original release time",
+	"TDRC": "Recording time",
+	"TDRL": "Release time",
+	"TDTG": "Tagging time",
+	"TENC": "Encoded by",
+	"TEXT": "Lyricist/Text writer",
+	"TFLT": "File type",
+	"TIPL": "Involved people list",
+	"TIT1": "Content group description",
+	"TIT2": "Title/songname/content description",
+	"TIT3": "Subtitle/Description refinement",
+	"TKEY": "Initial key",
+	"TLAN": "Language(s)",
+	"TLEN": "Length",
+	"TMCL": "Musician credits list",
+	"TMED": "Media type",
+	"TMOO": "Mood",
+	"TOAL": "Original album/movie/show title",
+	"TOFN": "Original filename",
+	"TOLY": "Original lyricist(s)/text writer(s)",
+	"TOPE": "Original artist(s)/performer(s)",
+	"TOWN": "File owner/licensee",
+	"TPE1": "Lead performer(s)/Soloist(s)",
+	"TPE2": "Band/orchestra/accompaniment",
+	"TPE3": "Conductor/performer refinement",
+	"TPE4": "Interpreted, remixed, or otherwise modified by",
+	"TPOS": "Part of a set",
+	"TPRO": "Produced notice",
+	"TPUB": "Publisher",
+	"TRCK": "Track number/Position in set",
+	"TRSN": "Internet radio station name",
+	"TRSO": "Internet radio station owner",
+	"TSO2": "iTunes uses this for Album Artist sort order",
+	"TSOA": "Album sort order",
+	"TSOC": "iTunes uses this for Composer sort order",
+	"TSOP": "Performer sort order",
+	"TSOT": "Title sort order",
+	"TSRC": "ISRC (international standard recording code)",
+	"TSSE": "Software/Hardware and settings used for encoding",
+	"TSST": "Set subtitle",
+	"TXXX": "User defined text information frame",
+
+	"UFID": "Unique file identifier",
+	"USER": "Terms of use",
+	"USLT": "Unsynchronised lyric/text transcription",
+
+	"WCOM": "Commercial information",
+	"WCOP": "Copyright/Legal information",
+	"WOAF": "Official audio file webpage",
+	"WOAR": "Official artist/performer webpage",
+	"WOAS": "Official audio source webpage",
+	"WORS": "Official Internet radio station homepage",
+	"WPAY": "Payment",
+	"WPUB": "Publishers official webpage",
+	"WXXX": "User defined URL link frame",
+}
+
+// ID3 frames that are defined in the specs.
+var id3Frames = map[Format]map[string]string{
+	ID3v2_2: id3v22Frames,
+	ID3v2_3: id3v23Frames,
+	ID3v2_4: id3v24Frames,
+}
+
+func validID3Frame(version Format, name string) bool {
+	names, ok := id3Frames[version]
+	if !ok {
+		return false
+	}
+	_, ok = names[name]
+	return ok
+}
+
+func readWFrame(b []byte) (string, error) {
+	// Frame text is always encoded in ISO-8859-1
+	b = append([]byte{0}, b...)
+	return readTFrame(b)
+}
+
+func readTFrame(b []byte) (string, error) {
+	if len(b) == 0 {
+		return "", nil
+	}
+
+	txt, err := decodeText(b[0], b[1:])
+	if err != nil {
+		return "", err
+	}
+	return strings.Join(strings.Split(txt, string(singleZero)), ""), nil
+}
+
+const (
+	encodingISO8859      byte = 0
+	encodingUTF16WithBOM byte = 1
+	encodingUTF16        byte = 2
+	encodingUTF8         byte = 3
+)
+
+func decodeText(enc byte, b []byte) (string, error) {
+	if len(b) == 0 {
+		return "", nil
+	}
+
+	switch enc {
+	case encodingISO8859: // ISO-8859-1
+		return decodeISO8859(b), nil
+
+	case encodingUTF16WithBOM: // UTF-16 with byte order marker
+		if len(b) == 1 {
+			return "", nil
+		}
+		return decodeUTF16WithBOM(b)
+
+	case encodingUTF16: // UTF-16 without byte order (assuming BigEndian)
+		if len(b) == 1 {
+			return "", nil
+		}
+		return decodeUTF16(b, binary.BigEndian)
+
+	case encodingUTF8: // UTF-8
+		return string(b), nil
+
+	default: // Fallback to ISO-8859-1
+		return decodeISO8859(b), nil
+	}
+}
+
+var (
+	singleZero = []byte{0}
+	doubleZero = []byte{0, 0}
+)
+
+func dataSplit(b []byte, enc byte) [][]byte {
+	delim := singleZero
+	if enc == encodingUTF16 || enc == encodingUTF16WithBOM {
+		delim = doubleZero
+	}
+
+	result := bytes.SplitN(b, delim, 2)
+	if len(result) != 2 {
+		return result
+	}
+
+	if len(result[1]) == 0 {
+		return result
+	}
+
+	if result[1][0] == 0 {
+		// there was a double (or triple) 0 and we cut too early
+		result[0] = append(result[0], result[1][0])
+		result[1] = result[1][1:]
+	}
+	return result
+}
+
+func decodeISO8859(b []byte) string {
+	r := make([]rune, len(b))
+	for i, x := range b {
+		r[i] = rune(x)
+	}
+	return string(r)
+}
+
+func decodeUTF16WithBOM(b []byte) (string, error) {
+	if len(b) < 2 {
+		return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark")
+	}
+
+	var bo binary.ByteOrder
+	switch {
+	case b[0] == 0xFE && b[1] == 0xFF:
+		bo = binary.BigEndian
+		b = b[2:]
+
+	case b[0] == 0xFF && b[1] == 0xFE:
+		bo = binary.LittleEndian
+		b = b[2:]
+
+	default:
+		bo = DefaultUTF16WithBOMByteOrder
+	}
+	return decodeUTF16(b, bo)
+}
+
+func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) {
+	if len(b)%2 != 0 {
+		return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text")
+	}
+	s := make([]uint16, 0, len(b)/2)
+	for i := 0; i < len(b); i += 2 {
+		s = append(s, bo.Uint16(b[i:i+2]))
+	}
+	return string(utf16.Decode(s)), nil
+}
+
+// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag.
+// It's a text with a description and a specified language
+// For WXXX, TXXX and UFID, we don't set a Language
+type Comm struct {
+	Language    string
+	Description string
+	Text        string
+}
+
+// String returns a string representation of the underlying Comm instance.
+func (t Comm) String() string {
+	if t.Language != "" {
+		return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}",
+			t.Language, t.Description, strings.Count(t.Text, "\n"))
+	}
+	return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text)
+}
+
+// IDv2.{3,4}
+// -- Header
+// <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT">
+// <Header for 'Comment', ID: "COMM">
+// -- readTextWithDescrFrame(data, true, true)
+// Text encoding       $xx
+// Language            $xx xx xx
+// Content descriptor  <text string according to encoding> $00 (00)
+// Lyrics/text         <full text string according to encoding>
+// -- Header
+// <Header for         'User defined text information frame', ID: "TXXX">
+// <Header for         'User defined URL link frame', ID: "WXXX">
+// -- readTextWithDescrFrame(data, false, <isDataEncoded>)
+// Text encoding       $xx
+// Description         <text string according to encoding> $00 (00)
+// Value               <text string according to encoding>
+func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) {
+	if len(b) == 0 {
+		return nil, errors.New("error decoding tag description text: invalid encoding")
+	}
+	enc := b[0]
+	b = b[1:]
+
+	c := &Comm{}
+	if hasLang {
+		if len(b) < 3 {
+			return nil, errors.New("hasLang set but not enough data for language information")
+		}
+		c.Language = string(b[:3])
+		b = b[3:]
+	}
+
+	descTextSplit := dataSplit(b, enc)
+	if len(descTextSplit) == 0 {
+		return nil, errors.New("error decoding tag description text: invalid encoding")
+	}
+
+	desc, err := decodeText(enc, descTextSplit[0])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding tag description text: %v", err)
+	}
+	c.Description = desc
+
+	if len(descTextSplit) == 1 {
+		return c, nil
+	}
+
+	if !encoded {
+		enc = byte(0)
+	}
+	text, err := decodeText(enc, descTextSplit[1])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding tag text: %v", err)
+	}
+	c.Text = text
+
+	return c, nil
+}
+
+// UFID is composed of a provider (frequently a URL and a binary identifier)
+// The identifier can be a text (Musicbrainz use texts, but not necessary)
+type UFID struct {
+	Provider   string
+	Identifier []byte
+}
+
+func (u UFID) String() string {
+	return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier))
+}
+
+func readUFID(b []byte) (*UFID, error) {
+	result := bytes.SplitN(b, singleZero, 2)
+	if len(result) != 2 {
+		return nil, errors.New("expected to split UFID data into 2 pieces")
+	}
+
+	return &UFID{
+		Provider:   string(result[0]),
+		Identifier: result[1],
+	}, nil
+}
+
+var pictureTypes = map[byte]string{
+	0x00: "Other",
+	0x01: "32x32 pixels 'file icon' (PNG only)",
+	0x02: "Other file icon",
+	0x03: "Cover (front)",
+	0x04: "Cover (back)",
+	0x05: "Leaflet page",
+	0x06: "Media (e.g. lable side of CD)",
+	0x07: "Lead artist/lead performer/soloist",
+	0x08: "Artist/performer",
+	0x09: "Conductor",
+	0x0A: "Band/Orchestra",
+	0x0B: "Composer",
+	0x0C: "Lyricist/text writer",
+	0x0D: "Recording Location",
+	0x0E: "During recording",
+	0x0F: "During performance",
+	0x10: "Movie/video screen capture",
+	0x11: "A bright coloured fish",
+	0x12: "Illustration",
+	0x13: "Band/artist logotype",
+	0x14: "Publisher/Studio logotype",
+}
+
+// Picture is a type which represents an attached picture extracted from metadata.
+type Picture struct {
+	Ext         string // Extension of the picture file.
+	MIMEType    string // MIMEType of the picture.
+	Type        string // Type of the picture (see pictureTypes).
+	Description string // Description.
+	Data        []byte // Raw picture data.
+}
+
+// String returns a string representation of the underlying Picture instance.
+func (p Picture) String() string {
+	return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}",
+		p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data))
+}
+
+// IDv2.2
+// -- Header
+// Attached picture   "PIC"
+// Frame size         $xx xx xx
+// -- readPICFrame
+// Text encoding      $xx
+// Image format       $xx xx xx
+// Picture type       $xx
+// Description        <textstring> $00 (00)
+// Picture data       <binary data>
+func readPICFrame(b []byte) (*Picture, error) {
+	if len(b) < 5 {
+		return nil, errors.New("invalid PIC frame")
+	}
+
+	enc := b[0]
+	ext := string(b[1:4])
+	picType := b[4]
+
+	descDataSplit := dataSplit(b[5:], enc)
+	if len(descDataSplit) != 2 {
+		return nil, errors.New("error decoding PIC description text: invalid encoding")
+	}
+	desc, err := decodeText(enc, descDataSplit[0])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding PIC description text: %v", err)
+	}
+
+	var mimeType string
+	switch ext {
+	case "jpeg", "jpg":
+		mimeType = "image/jpeg"
+	case "png":
+		mimeType = "image/png"
+	}
+
+	return &Picture{
+		Ext:         ext,
+		MIMEType:    mimeType,
+		Type:        pictureTypes[picType],
+		Description: desc,
+		Data:        descDataSplit[1],
+	}, nil
+}
+
+// IDv2.{3,4}
+// -- Header
+// <Header for 'Attached picture', ID: "APIC">
+// -- readAPICFrame
+// Text encoding   $xx
+// MIME type       <text string> $00
+// Picture type    $xx
+// Description     <text string according to encoding> $00 (00)
+// Picture data    <binary data>
+func readAPICFrame(b []byte) (*Picture, error) {
+	if len(b) == 0 {
+		return nil, errors.New("error decoding APIC: invalid encoding")
+	}
+	enc := b[0]
+	mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2)
+	if len(mimeDataSplit) != 2 {
+		return nil, errors.New("error decoding APIC: invalid encoding")
+	}
+
+	mimeType := string(mimeDataSplit[0])
+
+	b = mimeDataSplit[1]
+	if len(b) < 1 {
+		return nil, fmt.Errorf("error decoding APIC mimetype")
+	}
+	picType := b[0]
+
+	descDataSplit := dataSplit(b[1:], enc)
+	if len(descDataSplit) != 2 {
+		return nil, errors.New("error decoding APIC description text: invalid encoding")
+	}
+	desc, err := decodeText(enc, descDataSplit[0])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding APIC description text: %v", err)
+	}
+
+	var ext string
+	switch mimeType {
+	case "image/jpeg":
+		ext = "jpg"
+	case "image/png":
+		ext = "png"
+	}
+
+	return &Picture{
+		Ext:         ext,
+		MIMEType:    mimeType,
+		Type:        pictureTypes[picType],
+		Description: desc,
+		Data:        descDataSplit[1],
+	}, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2metadata.go b/vendor/github.com/dhowden/tag/id3v2metadata.go
new file mode 100644
index 00000000000..6185963a3a4
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2metadata.go
@@ -0,0 +1,141 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"strconv"
+	"strings"
+)
+
+type frameNames map[string][2]string
+
+func (f frameNames) Name(s string, fm Format) string {
+	l, ok := f[s]
+	if !ok {
+		return ""
+	}
+
+	switch fm {
+	case ID3v2_2:
+		return l[0]
+	case ID3v2_3:
+		return l[1]
+	case ID3v2_4:
+		if s == "year" {
+			return "TDRC"
+		}
+		return l[1]
+	}
+	return ""
+}
+
+var frames = frameNames(map[string][2]string{
+	"title":        [2]string{"TT2", "TIT2"},
+	"artist":       [2]string{"TP1", "TPE1"},
+	"album":        [2]string{"TAL", "TALB"},
+	"album_artist": [2]string{"TP2", "TPE2"},
+	"composer":     [2]string{"TCM", "TCOM"},
+	"year":         [2]string{"TYE", "TYER"},
+	"track":        [2]string{"TRK", "TRCK"},
+	"disc":         [2]string{"TPA", "TPOS"},
+	"genre":        [2]string{"TCO", "TCON"},
+	"picture":      [2]string{"PIC", "APIC"},
+	"lyrics":       [2]string{"", "USLT"},
+	"comment":      [2]string{"COM", "COMM"},
+})
+
+// metadataID3v2 is the implementation of Metadata used for ID3v2 tags.
+type metadataID3v2 struct {
+	header *id3v2Header
+	frames map[string]interface{}
+}
+
+func (m metadataID3v2) getString(k string) string {
+	v, ok := m.frames[k]
+	if !ok {
+		return ""
+	}
+	return v.(string)
+}
+
+func (m metadataID3v2) Format() Format              { return m.header.Version }
+func (m metadataID3v2) FileType() FileType          { return MP3 }
+func (m metadataID3v2) Raw() map[string]interface{} { return m.frames }
+
+func (m metadataID3v2) Title() string {
+	return m.getString(frames.Name("title", m.Format()))
+}
+
+func (m metadataID3v2) Artist() string {
+	return m.getString(frames.Name("artist", m.Format()))
+}
+
+func (m metadataID3v2) Album() string {
+	return m.getString(frames.Name("album", m.Format()))
+}
+
+func (m metadataID3v2) AlbumArtist() string {
+	return m.getString(frames.Name("album_artist", m.Format()))
+}
+
+func (m metadataID3v2) Composer() string {
+	return m.getString(frames.Name("composer", m.Format()))
+}
+
+func (m metadataID3v2) Genre() string {
+	return id3v2genre(m.getString(frames.Name("genre", m.Format())))
+}
+
+func (m metadataID3v2) Year() int {
+	year, _ := strconv.Atoi(m.getString(frames.Name("year", m.Format())))
+	return year
+}
+
+func parseXofN(s string) (x, n int) {
+	xn := strings.Split(s, "/")
+	if len(xn) != 2 {
+		x, _ = strconv.Atoi(s)
+		return x, 0
+	}
+	x, _ = strconv.Atoi(strings.TrimSpace(xn[0]))
+	n, _ = strconv.Atoi(strings.TrimSpace(xn[1]))
+	return x, n
+}
+
+func (m metadataID3v2) Track() (int, int) {
+	return parseXofN(m.getString(frames.Name("track", m.Format())))
+}
+
+func (m metadataID3v2) Disc() (int, int) {
+	return parseXofN(m.getString(frames.Name("disc", m.Format())))
+}
+
+func (m metadataID3v2) Lyrics() string {
+	t, ok := m.frames[frames.Name("lyrics", m.Format())]
+	if !ok {
+		return ""
+	}
+	return t.(*Comm).Text
+}
+
+func (m metadataID3v2) Comment() string {
+	t, ok := m.frames[frames.Name("comment", m.Format())]
+	if !ok {
+		return ""
+	}
+	// id3v23 has Text, id3v24 has Description
+	if t.(*Comm).Description == "" {
+		return trimString(t.(*Comm).Text)
+	}
+	return trimString(t.(*Comm).Description)
+}
+
+func (m metadataID3v2) Picture() *Picture {
+	v, ok := m.frames[frames.Name("picture", m.Format())]
+	if !ok {
+		return nil
+	}
+	return v.(*Picture)
+}
diff --git a/vendor/github.com/dhowden/tag/mp4.go b/vendor/github.com/dhowden/tag/mp4.go
new file mode 100644
index 00000000000..6523397ddfa
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/mp4.go
@@ -0,0 +1,372 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+)
+
+var atomTypes = map[int]string{
+	0:  "implicit", // automatic based on atom name
+	1:  "text",
+	13: "jpeg",
+	14: "png",
+	21: "uint8",
+}
+
+// NB: atoms does not include "----", this is handled separately
+var atoms = atomNames(map[string]string{
+	"\xa9alb": "album",
+	"\xa9art": "artist",
+	"\xa9ART": "artist",
+	"aART":    "album_artist",
+	"\xa9day": "year",
+	"\xa9nam": "title",
+	"\xa9gen": "genre",
+	"trkn":    "track",
+	"\xa9wrt": "composer",
+	"\xa9too": "encoder",
+	"cprt":    "copyright",
+	"covr":    "picture",
+	"\xa9grp": "grouping",
+	"keyw":    "keyword",
+	"\xa9lyr": "lyrics",
+	"\xa9cmt": "comment",
+	"tmpo":    "tempo",
+	"cpil":    "compilation",
+	"disk":    "disc",
+})
+
+// Detect PNG image if "implicit" class is used
+var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
+
+type atomNames map[string]string
+
+func (f atomNames) Name(n string) []string {
+	res := make([]string, 1)
+	for k, v := range f {
+		if v == n {
+			res = append(res, k)
+		}
+	}
+	return res
+}
+
+// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data.
+type metadataMP4 struct {
+	fileType FileType
+	data     map[string]interface{}
+}
+
+// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning
+// non-nil error if there was a problem.
+func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
+	m := metadataMP4{
+		data:     make(map[string]interface{}),
+		fileType: UnknownFileType,
+	}
+	err := m.readAtoms(r)
+	return m, err
+}
+
+func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
+	for {
+		name, size, err := readAtomHeader(r)
+		if err != nil {
+			if err == io.EOF {
+				return nil
+			}
+			return err
+		}
+
+		switch name {
+		case "meta":
+			// next_item_id (int32)
+			_, err := readBytes(r, 4)
+			if err != nil {
+				return err
+			}
+			fallthrough
+
+		case "moov", "udta", "ilst":
+			return m.readAtoms(r)
+		}
+
+		_, ok := atoms[name]
+		var data []string
+		if name == "----" {
+			name, data, err = readCustomAtom(r, size)
+			if err != nil {
+				return err
+			}
+
+			if name != "----" {
+				ok = true
+				size = 0 // already read data
+			}
+		}
+
+		if !ok {
+			_, err := r.Seek(int64(size-8), io.SeekCurrent)
+			if err != nil {
+				return err
+			}
+			continue
+		}
+
+		err = m.readAtomData(r, name, size-8, data)
+		if err != nil {
+			return err
+		}
+	}
+}
+
+func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error {
+	var b []byte
+	var err error
+	var contentType string
+	if len(processedData) > 0 {
+		b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields
+		contentType = "text"
+	} else {
+		// read the data
+		b, err = readBytes(r, uint(size))
+		if err != nil {
+			return err
+		}
+		if len(b) < 8 {
+			return fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 8, len(b))
+		}
+
+		// "data" + size (4 bytes each)
+		b = b[8:]
+
+		if len(b) < 4 {
+			return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 4, len(b))
+		}
+		class := getInt(b[1:4])
+		var ok bool
+		contentType, ok = atomTypes[class]
+		if !ok {
+			return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
+		}
+
+		// 4: atom version (1 byte) + atom flags (3 bytes)
+		// 4: NULL (usually locale indicator)
+		if len(b) < 8 {
+			return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b))
+		}
+		b = b[8:]
+	}
+
+	if name == "trkn" || name == "disk" {
+		if len(b) < 6 {
+			return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b))
+		}
+
+		m.data[name] = int(b[3])
+		m.data[name+"_count"] = int(b[5])
+		return nil
+	}
+
+	if contentType == "implicit" {
+		if name == "covr" {
+			if bytes.HasPrefix(b, pngHeader) {
+				contentType = "png"
+			}
+			// TODO(dhowden): Detect JPEG formats too (harder).
+		}
+	}
+
+	var data interface{}
+	switch contentType {
+	case "implicit":
+		if _, ok := atoms[name]; ok {
+			return fmt.Errorf("unhandled implicit content type for required atom: %q", name)
+		}
+		return nil
+
+	case "text":
+		data = string(b)
+
+	case "uint8":
+		if len(b) < 1 {
+			return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
+		}
+		data = getInt(b[:1])
+
+	case "jpeg", "png":
+		data = &Picture{
+			Ext:      contentType,
+			MIMEType: "image/" + contentType,
+			Data:     b,
+		}
+	}
+	m.data[name] = data
+
+	return nil
+}
+
+func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
+	err = binary.Read(r, binary.BigEndian, &size)
+	if err != nil {
+		return
+	}
+	name, err = readString(r, 4)
+	return
+}
+
+// Generic atom.
+// Should have 3 sub atoms : mean, name and data.
+// We check that mean is "com.apple.iTunes" and we use the subname as
+// the name, and move to the data atom.
+// Data atom could have multiple data values, each with a header.
+// If anything goes wrong, we jump at the end of the "----" atom.
+func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) {
+	subNames := make(map[string]string)
+
+	for size > 8 {
+		subName, subSize, err := readAtomHeader(r)
+		if err != nil {
+			return "", nil, err
+		}
+
+		// Remove the size of the atom from the size counter
+		if size >= subSize {
+			size -= subSize
+		} else {
+			return "", nil, errors.New("--- invalid size")
+		}
+
+		b, err := readBytes(r, uint(subSize-8))
+		if err != nil {
+			return "", nil, err
+		}
+
+		if len(b) < 4 {
+			return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b))
+		}
+		switch subName {
+		case "mean", "name":
+			subNames[subName] = string(b[4:])
+		case "data":
+			data = append(data, string(b[4:]))
+		}
+	}
+
+	// there should remain only the header size
+	if size != 8 {
+		err := errors.New("---- atom out of bounds")
+		return "", nil, err
+	}
+
+	if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || len(data) == 0 {
+		return "----", nil, nil
+	}
+	return subNames["name"], data, nil
+}
+
+func (metadataMP4) Format() Format       { return MP4 }
+func (m metadataMP4) FileType() FileType { return m.fileType }
+
+func (m metadataMP4) Raw() map[string]interface{} { return m.data }
+
+func (m metadataMP4) getString(n []string) string {
+	for _, k := range n {
+		if x, ok := m.data[k]; ok {
+			return x.(string)
+		}
+	}
+	return ""
+}
+
+func (m metadataMP4) getInt(n []string) int {
+	for _, k := range n {
+		if x, ok := m.data[k]; ok {
+			return x.(int)
+		}
+	}
+	return 0
+}
+
+func (m metadataMP4) Title() string {
+	return m.getString(atoms.Name("title"))
+}
+
+func (m metadataMP4) Artist() string {
+	return m.getString(atoms.Name("artist"))
+}
+
+func (m metadataMP4) Album() string {
+	return m.getString(atoms.Name("album"))
+}
+
+func (m metadataMP4) AlbumArtist() string {
+	return m.getString(atoms.Name("album_artist"))
+}
+
+func (m metadataMP4) Composer() string {
+	return m.getString(atoms.Name("composer"))
+}
+
+func (m metadataMP4) Genre() string {
+	return m.getString(atoms.Name("genre"))
+}
+
+func (m metadataMP4) Year() int {
+	date := m.getString(atoms.Name("year"))
+	if len(date) >= 4 {
+		year, _ := strconv.Atoi(date[:4])
+		return year
+	}
+	return 0
+}
+
+func (m metadataMP4) Track() (int, int) {
+	x := m.getInt([]string{"trkn"})
+	if n, ok := m.data["trkn_count"]; ok {
+		return x, n.(int)
+	}
+	return x, 0
+}
+
+func (m metadataMP4) Disc() (int, int) {
+	x := m.getInt([]string{"disk"})
+	if n, ok := m.data["disk_count"]; ok {
+		return x, n.(int)
+	}
+	return x, 0
+}
+
+func (m metadataMP4) Lyrics() string {
+	t, ok := m.data["\xa9lyr"]
+	if !ok {
+		return ""
+	}
+	return t.(string)
+}
+
+func (m metadataMP4) Comment() string {
+	t, ok := m.data["\xa9cmt"]
+	if !ok {
+		return ""
+	}
+	return t.(string)
+}
+
+func (m metadataMP4) Picture() *Picture {
+	v, ok := m.data["covr"]
+	if !ok {
+		return nil
+	}
+	p, _ := v.(*Picture)
+	return p
+}
diff --git a/vendor/github.com/dhowden/tag/ogg.go b/vendor/github.com/dhowden/tag/ogg.go
new file mode 100644
index 00000000000..f5c4770c5a0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/ogg.go
@@ -0,0 +1,174 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+)
+
+var (
+	vorbisCommentPrefix = []byte("\x03vorbis")
+	opusTagsPrefix      = []byte("OpusTags")
+)
+
+var oggCRC32Poly04c11db7 = oggCRCTable(0x04c11db7)
+
+type crc32Table [256]uint32
+
+func oggCRCTable(poly uint32) *crc32Table {
+	var t crc32Table
+
+	for i := 0; i < 256; i++ {
+		crc := uint32(i) << 24
+		for j := 0; j < 8; j++ {
+			if crc&0x80000000 != 0 {
+				crc = (crc << 1) ^ poly
+			} else {
+				crc <<= 1
+			}
+		}
+		t[i] = crc
+	}
+
+	return &t
+}
+
+func oggCRCUpdate(crc uint32, tab *crc32Table, p []byte) uint32 {
+	for _, v := range p {
+		crc = (crc << 8) ^ tab[byte(crc>>24)^v]
+	}
+	return crc
+}
+
+type oggPageHeader struct {
+	Magic           [4]byte // "OggS"
+	Version         uint8
+	Flags           uint8
+	GranulePosition uint64
+	SerialNumber    uint32
+	SequenceNumber  uint32
+	CRC             uint32
+	Segments        uint8
+}
+
+type oggDemuxer struct {
+	packetBufs map[uint32]*bytes.Buffer
+}
+
+// Read ogg packets, can return empty slice of packets and nil err
+// if more data is needed
+func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) {
+	headerBuf := &bytes.Buffer{}
+	var oh oggPageHeader
+	if err := binary.Read(io.TeeReader(r, headerBuf), binary.LittleEndian, &oh); err != nil {
+		return nil, err
+	}
+
+	if bytes.Compare(oh.Magic[:], []byte("OggS")) != 0 {
+		// TODO: seek for syncword?
+		return nil, errors.New("expected 'OggS'")
+	}
+
+	segmentTable := make([]byte, oh.Segments)
+	if _, err := io.ReadFull(r, segmentTable); err != nil {
+		return nil, err
+	}
+	var segmentsSize int64
+	for _, s := range segmentTable {
+		segmentsSize += int64(s)
+	}
+	segmentsData := make([]byte, segmentsSize)
+	if _, err := io.ReadFull(r, segmentsData); err != nil {
+		return nil, err
+	}
+
+	headerBytes := headerBuf.Bytes()
+	// reset CRC to zero in header before checksum
+	headerBytes[22] = 0
+	headerBytes[23] = 0
+	headerBytes[24] = 0
+	headerBytes[25] = 0
+	crc := oggCRCUpdate(0, oggCRC32Poly04c11db7, headerBytes)
+	crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentTable)
+	crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentsData)
+	if crc != oh.CRC {
+		return nil, fmt.Errorf("expected crc %x != %x", oh.CRC, crc)
+	}
+
+	if o.packetBufs == nil {
+		o.packetBufs = map[uint32]*bytes.Buffer{}
+	}
+
+	var packetBuf *bytes.Buffer
+	continued := oh.Flags&0x1 != 0
+	if continued {
+		if b, ok := o.packetBufs[oh.SerialNumber]; ok {
+			packetBuf = b
+		} else {
+			return nil, fmt.Errorf("could not find continued packet %d", oh.SerialNumber)
+		}
+	} else {
+		packetBuf = &bytes.Buffer{}
+	}
+
+	var packets [][]byte
+	var p int
+	for _, s := range segmentTable {
+		packetBuf.Write(segmentsData[p : p+int(s)])
+		if s < 255 {
+			packets = append(packets, packetBuf.Bytes())
+			packetBuf = &bytes.Buffer{}
+		}
+		p += int(s)
+	}
+
+	o.packetBufs[oh.SerialNumber] = packetBuf
+
+	return packets, nil
+}
+
+// ReadOGGTags reads OGG metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+// See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html
+// and http://www.xiph.org/ogg/doc/framing.html for details.
+// For Opus see https://tools.ietf.org/html/rfc7845
+func ReadOGGTags(r io.Reader) (Metadata, error) {
+	od := &oggDemuxer{}
+	for {
+		bs, err := od.Read(r)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, b := range bs {
+			switch {
+			case bytes.HasPrefix(b, vorbisCommentPrefix):
+				m := &metadataOGG{
+					newMetadataVorbis(),
+				}
+				err = m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):]))
+				return m, err
+			case bytes.HasPrefix(b, opusTagsPrefix):
+				m := &metadataOGG{
+					newMetadataVorbis(),
+				}
+				err = m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):]))
+				return m, err
+			}
+		}
+	}
+}
+
+type metadataOGG struct {
+	*metadataVorbis
+}
+
+func (m *metadataOGG) FileType() FileType {
+	return OGG
+}
diff --git a/vendor/github.com/dhowden/tag/sum.go b/vendor/github.com/dhowden/tag/sum.go
new file mode 100644
index 00000000000..97b27a7edaa
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/sum.go
@@ -0,0 +1,219 @@
+package tag
+
+import (
+	"crypto/sha1"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"hash"
+	"io"
+)
+
+// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
+// (ID3, MP4) invariant.
+func Sum(r io.ReadSeeker) (string, error) {
+	b, err := readBytes(r, 11)
+	if err != nil {
+		return "", err
+	}
+
+	_, err = r.Seek(-11, io.SeekCurrent)
+	if err != nil {
+		return "", fmt.Errorf("could not seek back to original position: %v", err)
+	}
+
+	switch {
+	case string(b[0:4]) == "fLaC":
+		return SumFLAC(r)
+
+	case string(b[4:11]) == "ftypM4A":
+		return SumAtoms(r)
+
+	case string(b[0:3]) == "ID3":
+		return SumID3v2(r)
+	}
+
+	h, err := SumID3v1(r)
+	if err != nil {
+		if err == ErrNotID3v1 {
+			return SumAll(r)
+		}
+		return "", err
+	}
+	return h, nil
+}
+
+// SumAll returns a checksum of the content from the reader (until EOF).
+func SumAll(r io.ReadSeeker) (string, error) {
+	h := sha1.New()
+	_, err := io.Copy(h, r)
+	if err != nil {
+		return "", nil
+	}
+	return hashSum(h), nil
+}
+
+// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
+// metadata invariant.
+func SumAtoms(r io.ReadSeeker) (string, error) {
+	for {
+		var size uint32
+		err := binary.Read(r, binary.BigEndian, &size)
+		if err != nil {
+			if err == io.EOF {
+				return "", fmt.Errorf("reached EOF before audio data")
+			}
+			return "", err
+		}
+
+		name, err := readString(r, 4)
+		if err != nil {
+			return "", err
+		}
+
+		switch name {
+		case "meta":
+			// next_item_id (int32)
+			_, err := r.Seek(4, io.SeekCurrent)
+			if err != nil {
+				return "", err
+			}
+			fallthrough
+
+		case "moov", "udta", "ilst":
+			continue
+
+		case "mdat": // stop when we get to the data
+			h := sha1.New()
+			_, err := io.CopyN(h, r, int64(size-8))
+			if err != nil {
+				return "", fmt.Errorf("error reading audio data: %v", err)
+			}
+			return hashSum(h), nil
+		}
+
+		_, err = r.Seek(int64(size-8), io.SeekCurrent)
+		if err != nil {
+			return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
+		}
+	}
+}
+
+func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) {
+	n, err := r.Seek(-128, io.SeekEnd)
+	if err != nil {
+		return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err)
+	}
+
+	_, err = r.Seek(-n, io.SeekCurrent)
+	if err != nil {
+		return 0, fmt.Errorf("error seeking back to original position: %v", err)
+	}
+	return n, nil
+}
+
+// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
+// by the io.ReadSeeker which is metadata invariant.
+func SumID3v1(r io.ReadSeeker) (string, error) {
+	n, err := sizeToEndOffset(r, 128)
+	if err != nil {
+		return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+	}
+
+	// TODO: improve this check???
+	if n <= 0 {
+		return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
+	}
+
+	h := sha1.New()
+	_, err = io.CopyN(h, r, n)
+	if err != nil {
+		return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+	}
+	return hashSum(h), nil
+}
+
+// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
+// io.ReadSeeker which is metadata invariant.
+func SumID3v2(r io.ReadSeeker) (string, error) {
+	header, _, err := readID3v2Header(r)
+	if err != nil {
+		return "", fmt.Errorf("error reading ID3v2 header: %v", err)
+	}
+
+	_, err = r.Seek(int64(header.Size), io.SeekCurrent)
+	if err != nil {
+		return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
+	}
+
+	n, err := sizeToEndOffset(r, 128)
+	if err != nil {
+		return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+	}
+
+	// TODO: remove this check?????
+	if n < 0 {
+		return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
+	}
+
+	h := sha1.New()
+	_, err = io.CopyN(h, r, n)
+	if err != nil {
+		return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+	}
+	return hashSum(h), nil
+}
+
+// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores
+// metadata fields).
+func SumFLAC(r io.ReadSeeker) (string, error) {
+	flac, err := readString(r, 4)
+	if err != nil {
+		return "", err
+	}
+	if flac != "fLaC" {
+		return "", errors.New("expected 'fLaC'")
+	}
+
+	for {
+		last, err := skipFLACMetadataBlock(r)
+		if err != nil {
+			return "", err
+		}
+
+		if last {
+			break
+		}
+	}
+
+	h := sha1.New()
+	_, err = io.Copy(h, r)
+	if err != nil {
+		return "", fmt.Errorf("error reading data bytes from FLAC: %v", err)
+	}
+	return hashSum(h), nil
+}
+
+func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+	blockHeader, err := readBytes(r, 1)
+	if err != nil {
+		return
+	}
+
+	if getBit(blockHeader[0], 7) {
+		blockHeader[0] ^= (1 << 7)
+		last = true
+	}
+
+	blockLen, err := readInt(r, 3)
+	if err != nil {
+		return
+	}
+
+	_, err = r.Seek(int64(blockLen), io.SeekCurrent)
+	return
+}
+
+func hashSum(h hash.Hash) string {
+	return fmt.Sprintf("%x", h.Sum([]byte{}))
+}
diff --git a/vendor/github.com/dhowden/tag/tag.go b/vendor/github.com/dhowden/tag/tag.go
new file mode 100644
index 00000000000..306f1d7df1a
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/tag.go
@@ -0,0 +1,147 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tag provides MP3 (ID3: v1, 2.2, 2.3 and 2.4), MP4, FLAC and OGG metadata detection,
+// parsing and artwork extraction.
+//
+// Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File):
+// 	m, err := tag.ReadFrom(f)
+// 	if err != nil {
+// 		log.Fatal(err)
+// 	}
+// 	log.Print(m.Format()) // The detected format.
+// 	log.Print(m.Title())  // The title of the track (see Metadata interface for more details).
+package tag
+
+import (
+	"errors"
+	"fmt"
+	"io"
+)
+
+// ErrNoTagsFound is the error returned by ReadFrom when the metadata format
+// cannot be identified.
+var ErrNoTagsFound = errors.New("no tags found")
+
+// ReadFrom detects and parses audio file metadata tags (currently supports ID3v1,2.{2,3,4}, MP4, FLAC/OGG).
+// Returns non-nil error if the format of the given data could not be determined, or if there was a problem
+// parsing the data.
+func ReadFrom(r io.ReadSeeker) (Metadata, error) {
+	b, err := readBytes(r, 11)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = r.Seek(-11, io.SeekCurrent)
+	if err != nil {
+		return nil, fmt.Errorf("could not seek back to original position: %v", err)
+	}
+
+	switch {
+	case string(b[0:4]) == "fLaC":
+		return ReadFLACTags(r)
+
+	case string(b[0:4]) == "OggS":
+		return ReadOGGTags(r)
+
+	case string(b[4:8]) == "ftyp":
+		return ReadAtoms(r)
+
+	case string(b[0:3]) == "ID3":
+		return ReadID3v2Tags(r)
+
+	case string(b[0:4]) == "DSD ":
+		return ReadDSFTags(r)
+	}
+
+	m, err := ReadID3v1Tags(r)
+	if err != nil {
+		if err == ErrNotID3v1 {
+			err = ErrNoTagsFound
+		}
+		return nil, err
+	}
+	return m, nil
+}
+
+// Format is an enumeration of metadata types supported by this package.
+type Format string
+
+// Supported tag formats.
+const (
+	UnknownFormat Format = ""        // Unknown Format.
+	ID3v1         Format = "ID3v1"   // ID3v1 tag format.
+	ID3v2_2       Format = "ID3v2.2" // ID3v2.2 tag format.
+	ID3v2_3       Format = "ID3v2.3" // ID3v2.3 tag format (most common).
+	ID3v2_4       Format = "ID3v2.4" // ID3v2.4 tag format.
+	MP4           Format = "MP4"     // MP4 tag (atom) format (see http://www.ftyps.com/ for a full file type list)
+	VORBIS        Format = "VORBIS"  // Vorbis Comment tag format.
+)
+
+// FileType is an enumeration of the audio file types supported by this package, in particular
+// there are audio file types which share metadata formats, and this type is used to distinguish
+// between them.
+type FileType string
+
+// Supported file types.
+const (
+	UnknownFileType FileType = ""     // Unknown FileType.
+	MP3             FileType = "MP3"  // MP3 file
+	M4A             FileType = "M4A"  // M4A file Apple iTunes (ACC) Audio
+	M4B             FileType = "M4B"  // M4A file Apple iTunes (ACC) Audio Book
+	M4P             FileType = "M4P"  // M4A file Apple iTunes (ACC) AES Protected Audio
+	ALAC            FileType = "ALAC" // Apple Lossless file FIXME: actually detect this
+	FLAC            FileType = "FLAC" // FLAC file
+	OGG             FileType = "OGG"  // OGG file
+	DSF             FileType = "DSF"  // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf
+)
+
+// Metadata is an interface which is used to describe metadata retrieved by this package.
+type Metadata interface {
+	// Format returns the metadata Format used to encode the data.
+	Format() Format
+
+	// FileType returns the file type of the audio file.
+	FileType() FileType
+
+	// Title returns the title of the track.
+	Title() string
+
+	// Album returns the album name of the track.
+	Album() string
+
+	// Artist returns the artist name of the track.
+	Artist() string
+
+	// AlbumArtist returns the album artist name of the track.
+	AlbumArtist() string
+
+	// Composer returns the composer of the track.
+	Composer() string
+
+	// Year returns the year of the track.
+	Year() int
+
+	// Genre returns the genre of the track.
+	Genre() string
+
+	// Track returns the track number and total tracks, or zero values if unavailable.
+	Track() (int, int)
+
+	// Disc returns the disc number and total discs, or zero values if unavailable.
+	Disc() (int, int)
+
+	// Picture returns a picture, or nil if not available.
+	Picture() *Picture
+
+	// Lyrics returns the lyrics, or an empty string if unavailable.
+	Lyrics() string
+
+	// Comment returns the comment, or an empty string if unavailable.
+	Comment() string
+
+	// Raw returns the raw mapping of retrieved tag names and associated values.
+	// NB: tag/atom names are not standardised between formats.
+	Raw() map[string]interface{}
+}
diff --git a/vendor/github.com/dhowden/tag/util.go b/vendor/github.com/dhowden/tag/util.go
new file mode 100644
index 00000000000..c738d2fbbf1
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/util.go
@@ -0,0 +1,102 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"bytes"
+	"encoding/binary"
+	"io"
+)
+
+func getBit(b byte, n uint) bool {
+	x := byte(1 << n)
+	return (b & x) == x
+}
+
+func get7BitChunkedInt(b []byte) int {
+	var n int
+	for _, x := range b {
+		n = n << 7
+		n |= int(x)
+	}
+	return n
+}
+
+func getInt(b []byte) int {
+	var n int
+	for _, x := range b {
+		n = n << 8
+		n |= int(x)
+	}
+	return n
+}
+
+func readUint64LittleEndian(r io.Reader) (uint64, error) {
+	b, err := readBytes(r, 8)
+	if err != nil {
+		return 0, err
+	}
+	return binary.LittleEndian.Uint64(b), nil
+}
+
+// readBytesMaxUpfront is the max up-front allocation allowed
+const readBytesMaxUpfront = 10 << 20 // 10MB
+
+func readBytes(r io.Reader, n uint) ([]byte, error) {
+	if n > readBytesMaxUpfront {
+		b := &bytes.Buffer{}
+		if _, err := io.CopyN(b, r, int64(n)); err != nil {
+			return nil, err
+		}
+		return b.Bytes(), nil
+	}
+
+	b := make([]byte, n)
+	_, err := io.ReadFull(r, b)
+	if err != nil {
+		return nil, err
+	}
+	return b, nil
+}
+
+func readString(r io.Reader, n uint) (string, error) {
+	b, err := readBytes(r, n)
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}
+
+func readUint(r io.Reader, n uint) (uint, error) {
+	x, err := readInt(r, n)
+	if err != nil {
+		return 0, err
+	}
+	return uint(x), nil
+}
+
+func readInt(r io.Reader, n uint) (int, error) {
+	b, err := readBytes(r, n)
+	if err != nil {
+		return 0, err
+	}
+	return getInt(b), nil
+}
+
+func read7BitChunkedUint(r io.Reader, n uint) (uint, error) {
+	b, err := readBytes(r, n)
+	if err != nil {
+		return 0, err
+	}
+	return uint(get7BitChunkedInt(b)), nil
+}
+
+func readUint32LittleEndian(r io.Reader) (uint32, error) {
+	b, err := readBytes(r, 4)
+	if err != nil {
+		return 0, err
+	}
+	return binary.LittleEndian.Uint32(b), nil
+}
diff --git a/vendor/github.com/dhowden/tag/vorbis.go b/vendor/github.com/dhowden/tag/vorbis.go
new file mode 100644
index 00000000000..84332af93f4
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/vorbis.go
@@ -0,0 +1,259 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func newMetadataVorbis() *metadataVorbis {
+	return &metadataVorbis{
+		c: make(map[string]string),
+	}
+}
+
+type metadataVorbis struct {
+	c map[string]string // the vorbis comments
+	p *Picture
+}
+
+func (m *metadataVorbis) readVorbisComment(r io.Reader) error {
+	vendorLen, err := readUint32LittleEndian(r)
+	if err != nil {
+		return err
+	}
+
+	vendor, err := readString(r, uint(vendorLen))
+	if err != nil {
+		return err
+	}
+	m.c["vendor"] = vendor
+
+	commentsLen, err := readUint32LittleEndian(r)
+	if err != nil {
+		return err
+	}
+
+	for i := uint32(0); i < commentsLen; i++ {
+		l, err := readUint32LittleEndian(r)
+		if err != nil {
+			return err
+		}
+		s, err := readString(r, uint(l))
+		if err != nil {
+			return err
+		}
+		k, v, err := parseComment(s)
+		if err != nil {
+			return err
+		}
+		m.c[strings.ToLower(k)] = v
+	}
+
+	if b64data, ok := m.c["metadata_block_picture"]; ok {
+		data, err := base64.StdEncoding.DecodeString(b64data)
+		if err != nil {
+			return err
+		}
+		m.readPictureBlock(bytes.NewReader(data))
+	}
+
+	return nil
+}
+
+func (m *metadataVorbis) readPictureBlock(r io.Reader) error {
+	b, err := readInt(r, 4)
+	if err != nil {
+		return err
+	}
+	pictureType, ok := pictureTypes[byte(b)]
+	if !ok {
+		return fmt.Errorf("invalid picture type: %v", b)
+	}
+	mimeLen, err := readUint(r, 4)
+	if err != nil {
+		return err
+	}
+	mime, err := readString(r, mimeLen)
+	if err != nil {
+		return err
+	}
+
+	ext := ""
+	switch mime {
+	case "image/jpeg":
+		ext = "jpg"
+	case "image/png":
+		ext = "png"
+	case "image/gif":
+		ext = "gif"
+	}
+
+	descLen, err := readUint(r, 4)
+	if err != nil {
+		return err
+	}
+	desc, err := readString(r, descLen)
+	if err != nil {
+		return err
+	}
+
+	// We skip width <32>, height <32>, colorDepth <32>, coloresUsed <32>
+	_, err = readInt(r, 4) // width
+	if err != nil {
+		return err
+	}
+	_, err = readInt(r, 4) // height
+	if err != nil {
+		return err
+	}
+	_, err = readInt(r, 4) // color depth
+	if err != nil {
+		return err
+	}
+	_, err = readInt(r, 4) // colors used
+	if err != nil {
+		return err
+	}
+
+	dataLen, err := readInt(r, 4)
+	if err != nil {
+		return err
+	}
+	data := make([]byte, dataLen)
+	_, err = io.ReadFull(r, data)
+	if err != nil {
+		return err
+	}
+
+	m.p = &Picture{
+		Ext:         ext,
+		MIMEType:    mime,
+		Type:        pictureType,
+		Description: desc,
+		Data:        data,
+	}
+	return nil
+}
+
+func parseComment(c string) (k, v string, err error) {
+	kv := strings.SplitN(c, "=", 2)
+	if len(kv) != 2 {
+		err = errors.New("vorbis comment must contain '='")
+		return
+	}
+	k = kv[0]
+	v = kv[1]
+	return
+}
+
+func (m *metadataVorbis) Format() Format {
+	return VORBIS
+}
+
+func (m *metadataVorbis) Raw() map[string]interface{} {
+	raw := make(map[string]interface{}, len(m.c))
+	for k, v := range m.c {
+		raw[k] = v
+	}
+	return raw
+}
+
+func (m *metadataVorbis) Title() string {
+	return m.c["title"]
+}
+
+func (m *metadataVorbis) Artist() string {
+	// ARTIST
+	// The artist generally considered responsible for the work. In popular music
+	// this is usually the performing band or singer. For classical music it would
+	// be the composer. For an audio book it would be the author of the original text.
+	return m.c["artist"]
+}
+
+func (m *metadataVorbis) Album() string {
+	return m.c["album"]
+}
+
+func (m *metadataVorbis) AlbumArtist() string {
+	// This field isn't actually included in the standard, though
+	// it is commonly assigned to albumartist.
+	return m.c["albumartist"]
+}
+
+func (m *metadataVorbis) Composer() string {
+	if m.c["composer"] != "" {
+		return m.c["composer"]
+	}
+	// PERFORMER
+	// The artist(s) who performed the work. In classical music this would be the
+	// conductor, orchestra, soloists. In an audio book it would be the actor who
+	// did the reading. In popular music this is typically the same as the ARTIST
+	// and is omitted.
+	if m.c["performer"] != "" {
+		return m.c["performer"]
+	}
+	return m.c["artist"]
+}
+
+func (m *metadataVorbis) Genre() string {
+	return m.c["genre"]
+}
+
+func (m *metadataVorbis) Year() int {
+	var dateFormat string
+
+	// The date need to follow the international standard https://en.wikipedia.org/wiki/ISO_8601
+	// and obviously the VorbisComment standard https://wiki.xiph.org/VorbisComment#Date_and_time
+	switch len(m.c["date"]) {
+	case 0:
+		return 0
+	case 4:
+		dateFormat = "2006"
+	case 7:
+		dateFormat = "2006-01"
+	case 10:
+		dateFormat = "2006-01-02"
+	}
+
+	t, _ := time.Parse(dateFormat, m.c["date"])
+	return t.Year()
+}
+
+func (m *metadataVorbis) Track() (int, int) {
+	x, _ := strconv.Atoi(m.c["tracknumber"])
+	// https://wiki.xiph.org/Field_names
+	n, _ := strconv.Atoi(m.c["tracktotal"])
+	return x, n
+}
+
+func (m *metadataVorbis) Disc() (int, int) {
+	// https://wiki.xiph.org/Field_names
+	x, _ := strconv.Atoi(m.c["discnumber"])
+	n, _ := strconv.Atoi(m.c["disctotal"])
+	return x, n
+}
+
+func (m *metadataVorbis) Lyrics() string {
+	return m.c["lyrics"]
+}
+
+func (m *metadataVorbis) Comment() string {
+	if m.c["comment"] != "" {
+		return m.c["comment"]
+	}
+	return m.c["description"]
+}
+
+func (m *metadataVorbis) Picture() *Picture {
+	return m.p
+}
diff --git a/vendor/github.com/gabriel-vasile/mimetype/README.md b/vendor/github.com/gabriel-vasile/mimetype/README.md
index d310928deac..231b29190fd 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/README.md
+++ b/vendor/github.com/gabriel-vasile/mimetype/README.md
@@ -10,9 +10,6 @@
 </h6>
 
 <p align="center">
-  <a href="https://travis-ci.org/gabriel-vasile/mimetype">
-    <img alt="Build Status" src="https://travis-ci.org/gabriel-vasile/mimetype.svg?branch=master">
-  </a>
   <a href="https://pkg.go.dev/github.com/gabriel-vasile/mimetype">
     <img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gabriel-vasile/mimetype.svg">
   </a>
diff --git a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/binary.go b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/binary.go
index 29bdded3e8c..f1e944987c3 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/binary.go
+++ b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/binary.go
@@ -150,32 +150,34 @@ func Marc(raw []byte, limit uint32) bool {
 }
 
 // Glb matches a glTF model format file.
-// GLB is the binary file format representation of 3D models save in
+// GLB is the binary file format representation of 3D models saved in
 // the GL transmission Format (glTF).
-// see more: https://docs.fileformat.com/3d/glb/
-//           https://www.iana.org/assignments/media-types/model/gltf-binary
-// GLB file format is based on little endian and its header structure
-// show  below:
+// GLB uses little endian and its header structure is as follows:
 //
-// <-- 12-byte header                             -->
-// | magic            | version          | length   |
-// | (uint32)         | (uint32)         | (uint32) |
-// | \x67\x6C\x54\x46 | \x01\x00\x00\x00 | ...      |
-// | g   l   T   F    | 1                | ...      |
+// 	<-- 12-byte header                             -->
+// 	| magic            | version          | length   |
+// 	| (uint32)         | (uint32)         | (uint32) |
+// 	| \x67\x6C\x54\x46 | \x01\x00\x00\x00 | ...      |
+// 	| g   l   T   F    | 1                | ...      |
+//
+// Visit [glTF specification] and [IANA glTF entry] for more details.
+//
+// [glTF specification]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
+// [IANA glTF entry]: https://www.iana.org/assignments/media-types/model/gltf-binary
 var Glb = prefix([]byte("\x67\x6C\x54\x46\x02\x00\x00\x00"),
 	[]byte("\x67\x6C\x54\x46\x01\x00\x00\x00"))
 
 // TzIf matches a Time Zone Information Format (TZif) file.
 // See more: https://tools.ietf.org/id/draft-murchison-tzdist-tzif-00.html#rfc.section.3
 // Its header structure is shown below:
-// +---------------+---+
-// |  magic    (4) | <-+-- version (1)
-// +---------------+---+---------------------------------------+
-// |           [unused - reserved for future use] (15)         |
-// +---------------+---------------+---------------+-----------+
-// |  isutccnt (4) |  isstdcnt (4) |  leapcnt  (4) |
-// +---------------+---------------+---------------+
-// |  timecnt  (4) |  typecnt  (4) |  charcnt  (4) |
+// 	+---------------+---+
+// 	|  magic    (4) | <-+-- version (1)
+// 	+---------------+---+---------------------------------------+
+// 	|           [unused - reserved for future use] (15)         |
+// 	+---------------+---------------+---------------+-----------+
+// 	|  isutccnt (4) |  isstdcnt (4) |  leapcnt  (4) |
+// 	+---------------+---------------+---------------+
+// 	|  timecnt  (4) |  typecnt  (4) |  charcnt  (4) |
 func TzIf(raw []byte, limit uint32) bool {
 	// File is at least 44 bytes (header size).
 	if len(raw) < 44 {
diff --git a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/magic.go b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/magic.go
index 466058fbe24..34b84f401bd 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/magic.go
+++ b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/magic.go
@@ -177,7 +177,9 @@ func newXMLSig(localName, xmlns string) xmlSig {
 // and, optionally, followed by the arguments for the interpreter.
 //
 // Ex:
-//  #! /usr/bin/env php
+//
+//	#! /usr/bin/env php
+//
 // /usr/bin/env is the interpreter, php is the first and only argument.
 func shebang(sigs ...[]byte) Detector {
 	return func(raw []byte, limit uint32) bool {
diff --git a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/text_csv.go b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/text_csv.go
index 6a1561923c0..84ed6492840 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/internal/magic/text_csv.go
+++ b/vendor/github.com/gabriel-vasile/mimetype/internal/magic/text_csv.go
@@ -3,6 +3,7 @@ package magic
 import (
 	"bytes"
 	"encoding/csv"
+	"errors"
 	"io"
 )
 
@@ -19,12 +20,23 @@ func Tsv(raw []byte, limit uint32) bool {
 func sv(in []byte, comma rune, limit uint32) bool {
 	r := csv.NewReader(dropLastLine(in, limit))
 	r.Comma = comma
-	r.TrimLeadingSpace = true
+	r.ReuseRecord = true
 	r.LazyQuotes = true
 	r.Comment = '#'
 
-	lines, err := r.ReadAll()
-	return err == nil && r.FieldsPerRecord > 1 && len(lines) > 1
+	lines := 0
+	for {
+		_, err := r.Read()
+		if errors.Is(err, io.EOF) {
+			break
+		}
+		if err != nil {
+			return false
+		}
+		lines++
+	}
+
+	return r.FieldsPerRecord > 1 && lines > 1
 }
 
 // dropLastLine drops the last incomplete line from b.
diff --git a/vendor/github.com/gabriel-vasile/mimetype/mimetype.go b/vendor/github.com/gabriel-vasile/mimetype/mimetype.go
index 08e68e334a8..1b5909b751b 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/mimetype.go
+++ b/vendor/github.com/gabriel-vasile/mimetype/mimetype.go
@@ -39,7 +39,8 @@ func Detect(in []byte) *MIME {
 //
 // DetectReader assumes the reader offset is at the start. If the input is an
 // io.ReadSeeker you previously read from, it should be rewinded before detection:
-//  reader.Seek(0, io.SeekStart)
+//
+//	reader.Seek(0, io.SeekStart)
 func DetectReader(r io.Reader) (*MIME, error) {
 	var in []byte
 	var err error
diff --git a/vendor/github.com/gabriel-vasile/mimetype/supported_mimes.md b/vendor/github.com/gabriel-vasile/mimetype/supported_mimes.md
index cdec4e674e4..5ec6f6b6502 100644
--- a/vendor/github.com/gabriel-vasile/mimetype/supported_mimes.md
+++ b/vendor/github.com/gabriel-vasile/mimetype/supported_mimes.md
@@ -1,4 +1,4 @@
-## 172 Supported MIME types
+## 173 Supported MIME types
 This file is automatically generated when running tests. Do not edit manually.
 
 Extension | MIME type | Aliases
diff --git a/vendor/modules.txt b/vendor/modules.txt
index e2cc47604b5..ec7e9a8adf7 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -733,6 +733,9 @@ github.com/dgraph-io/ristretto/z/simd
 # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
 ## explicit
 github.com/dgryski/go-rendezvous
+# github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
+## explicit; go 1.18
+github.com/dhowden/tag
 # github.com/disintegration/imaging v1.6.2
 ## explicit
 github.com/disintegration/imaging
@@ -778,7 +781,7 @@ github.com/felixge/httpsnoop
 # github.com/fsnotify/fsnotify v1.6.0
 ## explicit; go 1.16
 github.com/fsnotify/fsnotify
-# github.com/gabriel-vasile/mimetype v1.4.2
+# github.com/gabriel-vasile/mimetype v1.4.3
 ## explicit; go 1.20
 github.com/gabriel-vasile/mimetype
 github.com/gabriel-vasile/mimetype/internal/charset


Test patch:
diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go
index 8b307d4330f..c8f8ff2082f 100644
--- a/services/graph/pkg/service/v0/driveitems_test.go
+++ b/services/graph/pkg/service/v0/driveitems_test.go
@@ -240,38 +240,113 @@ var _ = Describe("Driveitems", func() {
 			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
 		})
 
-		It("succeeds", func() {
-			mtime := time.Now()
-			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
-				Status: status.NewOK(ctx),
-				Infos: []*provider.ResourceInfo{
-					{
-						Type:  provider.ResourceType_RESOURCE_TYPE_FILE,
-						Id:    &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
-						Etag:  "etag",
-						Mtime: utils.TimeToTS(mtime),
+		Context("it succeeds", func() {
+			var (
+				r     *http.Request
+				mtime = time.Now()
+			)
+
+			BeforeEach(func() {
+				r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
+				rctx := chi.NewRouteContext()
+				rctx.URLParams.Add("driveID", "storageid$spaceid")
+				rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
+				r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			})
+
+			assertItemsList := func(length int) itemsList {
+				svc.GetDriveItemChildren(rr, r)
+				Expect(rr.Code).To(Equal(http.StatusOK))
+				data, err := io.ReadAll(rr.Body)
+				Expect(err).ToNot(HaveOccurred())
+
+				res := itemsList{}
+
+				err = json.Unmarshal(data, &res)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(len(res.Value)).To(Equal(1))
+				Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue())
+				Expect(res.Value[0].GetETag()).To(Equal("etag"))
+				Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid"))
+				Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid"))
+
+				return res
+			}
+
+			It("returns a generic file", func() {
+				gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+					Status: status.NewOK(ctx),
+					Infos: []*provider.ResourceInfo{
+						{
+							Type:              provider.ResourceType_RESOURCE_TYPE_FILE,
+							Id:                &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
+							Etag:              "etag",
+							Mtime:             utils.TimeToTS(mtime),
+							ArbitraryMetadata: nil,
+						},
 					},
-				},
-			}, nil)
-			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
-			rctx := chi.NewRouteContext()
-			rctx.URLParams.Add("driveID", "storageid$spaceid")
-			rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
-			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.GetDriveItemChildren(rr, r)
-			Expect(rr.Code).To(Equal(http.StatusOK))
-			data, err := io.ReadAll(rr.Body)
-			Expect(err).ToNot(HaveOccurred())
-
-			res := itemsList{}
-
-			err = json.Unmarshal(data, &res)
-			Expect(err).ToNot(HaveOccurred())
-
-			Expect(len(res.Value)).To(Equal(1))
-			Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue())
-			Expect(res.Value[0].GetETag()).To(Equal("etag"))
-			Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid"))
+				}, nil)
+
+				res := assertItemsList(1)
+				Expect(res.Value[0].Audio).To(BeNil())
+			})
+
+			It("returns the audio facet if metadata is available", func() {
+				gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+					Status: status.NewOK(ctx),
+					Infos: []*provider.ResourceInfo{
+						{
+							Type:     provider.ResourceType_RESOURCE_TYPE_FILE,
+							Id:       &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
+							Etag:     "etag",
+							Mtime:    utils.TimeToTS(mtime),
+							MimeType: "audio/mpeg",
+							ArbitraryMetadata: &provider.ArbitraryMetadata{
+								Metadata: map[string]string{
+									"libre.graph.audio.album":             "Some Album",
+									"libre.graph.audio.albumArtist":       "Some AlbumArtist",
+									"libre.graph.audio.artist":            "Some Artist",
+									"libre.graph.audio.bitrate":           "192",
+									"libre.graph.audio.composers":         "Some Composers",
+									"libre.graph.audio.copyright":         "Some Copyright",
+									"libre.graph.audio.disc":              "2",
+									"libre.graph.audio.discCount":         "5",
+									"libre.graph.audio.duration":          "225000",
+									"libre.graph.audio.genre":             "Some Genre",
+									"libre.graph.audio.hasDrm":            "false",
+									"libre.graph.audio.isVariableBitrate": "true",
+									"libre.graph.audio.title":             "Some Title",
+									"libre.graph.audio.track":             "6",
+									"libre.graph.audio.trackCount":        "9",
+									"libre.graph.audio.year":              "1994",
+								},
+							},
+						},
+					},
+				}, nil)
+
+				res := assertItemsList(1)
+				audio := res.Value[0].Audio
+
+				Expect(audio).ToNot(BeNil())
+				Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album")))
+				Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist")))
+				Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist")))
+				Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192)))
+				Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers")))
+				Expect(audio.Copyright).To(Equal(libregraph.PtrString("Some Copyright")))
+				Expect(audio.Disc).To(Equal(libregraph.PtrInt32(2)))
+				Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5)))
+				Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000)))
+				Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre")))
+				Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false)))
+				Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true)))
+				Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title")))
+				Expect(audio.Track).To(Equal(libregraph.PtrInt32(6)))
+				Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(9)))
+				Expect(audio.Year).To(Equal(libregraph.PtrInt32(1994)))
+			})
 		})
 	})
 })
diff --git a/services/graph/pkg/service/v0/educationclasses_test.go b/services/graph/pkg/service/v0/educationclasses_test.go
index 24b7d5f0866..765b19e801c 100644
--- a/services/graph/pkg/service/v0/educationclasses_test.go
+++ b/services/graph/pkg/service/v0/educationclasses_test.go
@@ -18,7 +18,6 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	libregraph "github.com/owncloud/libre-graph-api-go"
-	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
 	"github.com/owncloud/ocis/v2/services/graph/mocks"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
@@ -85,7 +84,6 @@ var _ = Describe("EducationClass", func() {
 			service.EventsPublisher(&eventsPublisher),
 			service.WithIdentityBackend(identityBackend),
 			service.WithIdentityEducationBackend(identityEducationBackend),
-			service.Logger(log.NewLogger(log.Level("debug"))),
 		)
 	})
 
diff --git a/services/graph/pkg/service/v0/sharedbyme_test.go b/services/graph/pkg/service/v0/sharedbyme_test.go
new file mode 100644
index 00000000000..edfe78cfe9f
--- /dev/null
+++ b/services/graph/pkg/service/v0/sharedbyme_test.go
@@ -0,0 +1,502 @@
+package svc_test
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"time"
+
+	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
+	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
+	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
+	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
+	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
+	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
+	"github.com/cs3org/reva/v2/pkg/storagespace"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
+	"github.com/owncloud/ocis/v2/services/graph/mocks"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
+	identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
+	service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
+	"github.com/stretchr/testify/mock"
+	"google.golang.org/grpc"
+)
+
+var _ = Describe("sharedbyme", func() {
+	var (
+		svc             service.Service
+		ctx             context.Context
+		cfg             *config.Config
+		gatewayClient   *cs3mocks.GatewayAPIClient
+		gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
+		eventsPublisher mocks.Publisher
+		identityBackend *identitymocks.Backend
+
+		rr *httptest.ResponseRecorder
+	)
+	expiration := time.Now()
+	userShare := collaboration.Share{
+		Id: &collaboration.ShareId{
+			OpaqueId: "share-id",
+		},
+		ResourceId: &provider.ResourceId{
+			StorageId: "storageid",
+			SpaceId:   "spaceid",
+			OpaqueId:  "opaqueid",
+		},
+		Grantee: &provider.Grantee{
+			Type: provider.GranteeType_GRANTEE_TYPE_USER,
+			Id: &provider.Grantee_UserId{
+				UserId: &userpb.UserId{
+					OpaqueId: "user-id",
+				},
+			},
+		},
+	}
+	groupShare := collaboration.Share{
+		Id: &collaboration.ShareId{
+			OpaqueId: "share-id",
+		},
+		ResourceId: &provider.ResourceId{
+			StorageId: "storageid",
+			SpaceId:   "spaceid",
+			OpaqueId:  "opaqueid",
+		},
+		Grantee: &provider.Grantee{
+			Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
+			Id: &provider.Grantee_GroupId{
+				GroupId: &grouppb.GroupId{
+					OpaqueId: "group-id",
+				},
+			},
+		},
+	}
+	userShareWithExpiration := collaboration.Share{
+		Id: &collaboration.ShareId{
+			OpaqueId: "expire-share-id",
+		},
+		ResourceId: &provider.ResourceId{
+			StorageId: "storageid",
+			SpaceId:   "spaceid",
+			OpaqueId:  "expire-opaqueid",
+		},
+		Grantee: &provider.Grantee{
+			Type: provider.GranteeType_GRANTEE_TYPE_USER,
+			Id: &provider.Grantee_UserId{
+				UserId: &userpb.UserId{
+					OpaqueId: "user-id",
+				},
+			},
+		},
+		Expiration: utils.TimeToTS(expiration),
+	}
+
+	publicShare := link.PublicShare{
+		Id: &link.PublicShareId{
+			OpaqueId: "public-share-id",
+		},
+		Token: "public-share-token",
+		ResourceId: &provider.ResourceId{
+			StorageId: "storageid",
+			SpaceId:   "spaceid",
+			OpaqueId:  "public-share-opaqueid",
+		},
+	}
+
+	BeforeEach(func() {
+		eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+		rr = httptest.NewRecorder()
+
+		pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
+		gatewayClient = &cs3mocks.GatewayAPIClient{}
+
+		gatewayClient.On("Stat",
+			mock.Anything,
+			mock.MatchedBy(
+				func(req *provider.StatRequest) bool {
+					return req.Ref.ResourceId.OpaqueId == userShareWithExpiration.ResourceId.OpaqueId
+				})).
+			Return(&provider.StatResponse{
+				Status: status.NewOK(ctx),
+				Info: &provider.ResourceInfo{
+					Id: userShareWithExpiration.ResourceId,
+				},
+			}, nil)
+		gatewayClient.On("Stat",
+			mock.Anything,
+			mock.MatchedBy(
+				func(req *provider.StatRequest) bool {
+					return req.Ref.ResourceId.OpaqueId == publicShare.ResourceId.OpaqueId
+				})).
+			Return(&provider.StatResponse{
+				Status: status.NewOK(ctx),
+				Info: &provider.ResourceInfo{
+					Id: publicShare.ResourceId,
+				},
+			}, nil)
+		gatewayClient.On("Stat",
+			mock.Anything,
+			mock.Anything).
+			Return(&provider.StatResponse{
+				Status: status.NewOK(ctx),
+				Info: &provider.ResourceInfo{
+					Id: userShare.ResourceId,
+				},
+			}, nil)
+
+		gatewayClient.On("GetUser",
+			mock.Anything,
+			mock.MatchedBy(func(req *userpb.GetUserRequest) bool {
+				return req.UserId.OpaqueId == "user-id"
+			})).
+			Return(&userpb.GetUserResponse{
+				Status: status.NewOK(ctx),
+				User: &userpb.User{
+					Id: &userpb.UserId{
+						Idp:      "idp",
+						OpaqueId: "user-id",
+					},
+					DisplayName: "User Name",
+				},
+			}, nil)
+		gatewayClient.On("GetUser",
+			mock.Anything,
+			mock.Anything).
+			Return(&userpb.GetUserResponse{
+				Status: status.NewNotFound(ctx, "mock user not found"),
+				User:   nil,
+			}, nil)
+		gatewayClient.On("GetGroup",
+			mock.Anything,
+			mock.MatchedBy(func(req *grouppb.GetGroupRequest) bool {
+				return req.GroupId.OpaqueId == "group-id"
+			})).
+			Return(&grouppb.GetGroupResponse{
+				Status: status.NewOK(ctx),
+				Group: &grouppb.Group{
+					Id: &grouppb.GroupId{
+						Idp:      "idp",
+						OpaqueId: "group-id",
+					},
+					DisplayName: "Group Name",
+				},
+			}, nil)
+		gatewayClient.On("GetGroup",
+			mock.Anything,
+			mock.Anything).
+			Return(&grouppb.GetGroupResponse{
+				Status: status.NewNotFound(ctx, "mock group not found"),
+				Group:  nil,
+			}, nil)
+
+		gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
+			"GatewaySelector",
+			"com.owncloud.api.gateway",
+			func(cc *grpc.ClientConn) gateway.GatewayAPIClient {
+				return gatewayClient
+			},
+		)
+
+		identityBackend = &identitymocks.Backend{}
+
+		rr = httptest.NewRecorder()
+		ctx = context.Background()
+
+		cfg = defaults.FullDefaultConfig()
+		cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
+		cfg.TokenManager.JWTSecret = "loremipsum"
+		cfg.Commons = &shared.Commons{}
+		cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
+
+		svc, _ = service.NewService(
+			service.Config(cfg),
+			service.WithGatewaySelector(gatewaySelector),
+			service.EventsPublisher(&eventsPublisher),
+			service.WithIdentityBackend(identityBackend),
+		)
+	})
+
+	emptyListPublicSharesMock := func() {
+		gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(
+			&link.ListPublicSharesResponse{
+				Status: status.NewOK(ctx),
+				Share:  []*link.PublicShare{},
+			},
+			nil,
+		)
+	}
+	emptyListSharesMock := func() {
+		gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+			&collaboration.ListSharesResponse{
+				Status: status.NewOK(ctx),
+				Shares: []*collaboration.Share{},
+			},
+			nil,
+		)
+	}
+	Describe("GetSharedByMe", func() {
+		It("handles a failing ListShares", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(nil, errors.New("some error"))
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("handles ListShares returning an error status", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+				&collaboration.ListSharesResponse{Status: status.NewInternal(ctx, "error listing shares")},
+				nil,
+			)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("succeeds, when no shares are returned", func() {
+			emptyListPublicSharesMock()
+			emptyListSharesMock()
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(0))
+		})
+
+		It("returns a proper driveItem, when a single user share is returned", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+				&collaboration.ListSharesResponse{
+					Status: status.NewOK(ctx),
+					Shares: []*collaboration.Share{
+						&userShare,
+					},
+				},
+				nil,
+			)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+
+			di := res.Value[0]
+			Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShare.GetResourceId())))
+
+			perm := di.GetPermissions()
+			Expect(perm[0].GetId()).To(Equal(userShare.GetId().GetOpaqueId()))
+			_, ok := perm[0].GetExpirationDateTimeOk()
+			Expect(ok).To(BeFalse())
+			_, ok = perm[0].GrantedToV2.GetGroupOk()
+			Expect(ok).To(BeFalse())
+			user, ok := perm[0].GrantedToV2.GetUserOk()
+			Expect(ok).To(BeTrue())
+			Expect(user.GetId()).To(Equal(userShare.GetGrantee().GetUserId().GetOpaqueId()))
+			_, ok = perm[0].GetLinkOk()
+			Expect(ok).To(BeFalse())
+		})
+
+		It("returns a proper driveItem, when a single group share is returned", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+				&collaboration.ListSharesResponse{
+					Status: status.NewOK(ctx),
+					Shares: []*collaboration.Share{
+						&groupShare,
+					},
+				},
+				nil,
+			)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+
+			di := res.Value[0]
+			Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId())))
+
+			perm := di.GetPermissions()
+			Expect(perm[0].GetId()).To(Equal(userShare.GetId().GetOpaqueId()))
+			_, ok := perm[0].GetExpirationDateTimeOk()
+			Expect(ok).To(BeFalse())
+			_, ok = perm[0].GrantedToV2.GetUserOk()
+			Expect(ok).To(BeFalse())
+			group, ok := perm[0].GrantedToV2.GetGroupOk()
+			Expect(ok).To(BeTrue())
+			Expect(group.GetId()).To(Equal(groupShare.GetGrantee().GetGroupId().GetOpaqueId()))
+			_, ok = perm[0].GetLinkOk()
+			Expect(ok).To(BeFalse())
+		})
+
+		It("returns a single driveItem, when a mulitple shares for the same resource are returned", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+				&collaboration.ListSharesResponse{
+					Status: status.NewOK(ctx),
+					Shares: []*collaboration.Share{
+						&groupShare,
+						&userShare,
+					},
+				},
+				nil,
+			)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+
+			di := res.Value[0]
+			Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*groupShare.GetResourceId())))
+
+			// one permission per share
+			Expect(len(di.GetPermissions())).To(Equal(2))
+		})
+
+		It("return a driveItem with the expiration date set, for expiring shares", func() {
+			emptyListPublicSharesMock()
+			gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(
+				&collaboration.ListSharesResponse{
+					Status: status.NewOK(ctx),
+					Shares: []*collaboration.Share{
+						&userShareWithExpiration,
+					},
+				},
+				nil,
+			)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+
+			di := res.Value[0]
+			Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*userShareWithExpiration.GetResourceId())))
+
+			perm := di.GetPermissions()
+			Expect(perm[0].GetId()).To(Equal(userShareWithExpiration.GetId().GetOpaqueId()))
+			exp, ok := perm[0].GetExpirationDateTimeOk()
+			Expect(ok).To(BeTrue())
+			Expect(exp.Equal(expiration)).To(BeTrue())
+			_, ok = perm[0].GrantedToV2.GetGroupOk()
+			Expect(ok).To(BeFalse())
+			user, ok := perm[0].GrantedToV2.GetUserOk()
+			Expect(ok).To(BeTrue())
+			Expect(user.GetId()).To(Equal(userShareWithExpiration.GetGrantee().GetUserId().GetOpaqueId()))
+			_, ok = perm[0].GetLinkOk()
+			Expect(ok).To(BeFalse())
+		})
+
+		// Public Shares / "links" in graph terms
+		It("handles a failing ListPublicShares", func() {
+			gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(nil, errors.New("some error"))
+			emptyListSharesMock()
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("handles ListPublicShares returning an error status", func() {
+			emptyListSharesMock()
+			gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(
+				&link.ListPublicSharesResponse{Status: status.NewInternal(ctx, "error listing shares")},
+				nil,
+			)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("returns a proper driveItem, when a single public share is returned", func() {
+			emptyListSharesMock()
+			gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(
+				&link.ListPublicSharesResponse{
+					Status: status.NewOK(ctx),
+					Share: []*link.PublicShare{
+						&publicShare,
+					},
+				},
+				nil,
+			)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives/sharedByMe", nil)
+			svc.GetSharedByMe(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+
+			di := res.Value[0]
+			Expect(di.GetId()).To(Equal(storagespace.FormatResourceID(*publicShare.GetResourceId())))
+
+			perm := di.GetPermissions()
+			Expect(perm[0].GetId()).To(Equal(publicShare.GetId().GetOpaqueId()))
+			_, ok := perm[0].GetExpirationDateTimeOk()
+			Expect(ok).To(BeFalse())
+			_, ok = perm[0].GetGrantedToV2Ok()
+			Expect(ok).To(BeFalse())
+			link, ok := perm[0].GetLinkOk()
+			Expect(ok).To(BeTrue())
+			Expect(link.GetWebUrl()).To(Equal("https://localhost:9200/s/" + publicShare.GetToken()))
+		})
+
+	})
+})
diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go
index fbb5c0578b1..497247cac8d 100644
--- a/services/search/pkg/content/tika_test.go
+++ b/services/search/pkg/content/tika_test.go
@@ -13,6 +13,7 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/stretchr/testify/mock"
 
+	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
 	conf "github.com/owncloud/ocis/v2/services/search/pkg/config/defaults"
 	"github.com/owncloud/ocis/v2/services/search/pkg/content"
@@ -22,17 +23,19 @@ import (
 var _ = Describe("Tika", func() {
 	Describe("extract", func() {
 		var (
-			body     string
-			language string
-			version  string
-			srv      *httptest.Server
-			tika     *content.Tika
+			body         string
+			fullResponse string
+			language     string
+			version      string
+			srv          *httptest.Server
+			tika         *content.Tika
 		)
 
 		BeforeEach(func() {
 			body = ""
 			language = ""
 			version = ""
+			fullResponse = ""
 			srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 				out := ""
 				switch req.URL.Path {
@@ -41,7 +44,11 @@ var _ = Describe("Tika", func() {
 				case "/language/string":
 					out = language
 				case "/rmeta/text":
-					out = fmt.Sprintf(`[{"X-TIKA:content":"%s"}]`, body)
+					if fullResponse != "" {
+						out = fullResponse
+					} else {
+						out = fmt.Sprintf(`[{"X-TIKA:content":"%s"}]`, body)
+					}
 				}
 
 				_, _ = w.Write([]byte(out))
@@ -83,6 +90,56 @@ var _ = Describe("Tika", func() {
 			Expect(doc.Content).To(Equal(body))
 		})
 
+		It("adds audio content", func() {
+			fullResponse = `[
+				{
+					"xmpDM:genre": "Some Genre",
+					"xmpDM:album": "Some Album",
+					"xmpDM:trackNumber": "7",
+					"xmpDM:discNumber": "4",
+					"xmpDM:releaseDate": "2004",
+					"xmpDM:artist": "Some Artist",
+					"xmpDM:albumArtist": "Some AlbumArtist",
+					"xmpDM:audioCompressor": "MP3",
+					"xmpDM:audioChannelType": "Stereo",
+					"version": "MPEG 3 Layer III Version 1",
+					"xmpDM:logComment": "some comment",
+					"xmpDM:audioSampleRate": "44100",
+					"channels": "2",
+					"dc:title": "Some Title",
+					"xmpDM:duration": "225",
+					"Content-Type": "audio/mpeg",
+					"samplerate": "44100"
+				}
+			]`
+			doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{
+				Type: provider.ResourceType_RESOURCE_TYPE_FILE,
+				Size: 1,
+			})
+			Expect(err).ToNot(HaveOccurred())
+
+			audio := doc.Audio
+			Expect(audio).ToNot(BeNil())
+
+			Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album")))
+			Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist")))
+			Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist")))
+			// Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192)))
+			// Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers")))
+			// Expect(audio.Copyright).To(Equal(libregraph.PtrString("Some Copyright")))
+			Expect(audio.Disc).To(Equal(libregraph.PtrInt32(4)))
+			// Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5)))
+			Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000)))
+			Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre")))
+			// Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false)))
+			// Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true)))
+			Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title")))
+			Expect(audio.Track).To(Equal(libregraph.PtrInt32(7)))
+			// Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(9)))
+			Expect(audio.Year).To(Equal(libregraph.PtrInt32(2004)))
+
+		})
+
 		It("removes stop words", func() {
 			body = "body to test stop words!!! against almost everyone"
 			language = "en"
diff --git a/services/search/pkg/engine/bleve_test.go b/services/search/pkg/engine/bleve_test.go
index 53690a81d7a..8b9668faf47 100644
--- a/services/search/pkg/engine/bleve_test.go
+++ b/services/search/pkg/engine/bleve_test.go
@@ -10,6 +10,7 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 
+	libregraph "github.com/owncloud/libre-graph-api-go"
 	searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
 	searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
 	"github.com/owncloud/ocis/v2/services/search/pkg/content"
@@ -352,6 +353,7 @@ var _ = Describe("Bleve", func() {
 				Expect(res.TotalMatches).To(Equal(int32(1)))
 			})
 		})
+
 	})
 
 	Describe("Upsert", func() {
@@ -483,4 +485,68 @@ var _ = Describe("Bleve", func() {
 
 		})
 	})
+
+	Describe("File type specific metadata", func() {
+
+		Context("with audio metadata", func() {
+			BeforeEach(func() {
+				resource := engine.Resource{
+					ID:       "1$2!7",
+					ParentID: rootResource.ID,
+					RootID:   rootResource.ID,
+					Path:     "./some_song.mp3",
+					Type:     uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE),
+					Document: content.Document{
+						Name:     "some_song.mp3",
+						MimeType: "audio/mpeg",
+						Audio: &libregraph.Audio{
+							Album:             libregraph.PtrString("Some Album"),
+							AlbumArtist:       libregraph.PtrString("Some AlbumArtist"),
+							Artist:            libregraph.PtrString("Some Artist"),
+							Bitrate:           libregraph.PtrInt64(192),
+							Composers:         libregraph.PtrString("Some Composers"),
+							Copyright:         libregraph.PtrString(""),
+							Disc:              libregraph.PtrInt32(2),
+							DiscCount:         libregraph.PtrInt32(5),
+							Duration:          libregraph.PtrInt64(225000),
+							Genre:             libregraph.PtrString("Some Genre"),
+							HasDrm:            libregraph.PtrBool(false),
+							IsVariableBitrate: libregraph.PtrBool(true),
+							Title:             libregraph.PtrString("Some Title"),
+							Track:             libregraph.PtrInt32(34),
+							TrackCount:        libregraph.PtrInt32(99),
+							Year:              libregraph.PtrInt32(2004),
+						},
+					},
+				}
+				err := eng.Upsert(resource.ID, resource)
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			It("returns audio metadata for search", func() {
+				matches := assertDocCount(rootResource.ID, `*song*`, 1)
+				audio := matches[0].Entity.Audio
+
+				Expect(audio).ToNot(BeNil())
+
+				Expect(audio.Album).To(Equal(libregraph.PtrString("Some Album")))
+				Expect(audio.AlbumArtist).To(Equal(libregraph.PtrString("Some AlbumArtist")))
+				Expect(audio.Artist).To(Equal(libregraph.PtrString("Some Artist")))
+				Expect(audio.Bitrate).To(Equal(libregraph.PtrInt64(192)))
+				Expect(audio.Composers).To(Equal(libregraph.PtrString("Some Composers")))
+				Expect(audio.Copyright).To(Equal(libregraph.PtrString("")))
+				Expect(audio.Disc).To(Equal(libregraph.PtrInt32(2)))
+				Expect(audio.DiscCount).To(Equal(libregraph.PtrInt32(5)))
+				Expect(audio.Duration).To(Equal(libregraph.PtrInt64(225000)))
+				Expect(audio.Genre).To(Equal(libregraph.PtrString("Some Genre")))
+				Expect(audio.HasDrm).To(Equal(libregraph.PtrBool(false)))
+				Expect(audio.IsVariableBitrate).To(Equal(libregraph.PtrBool(true)))
+				Expect(audio.Title).To(Equal(libregraph.PtrString("Some Title")))
+				Expect(audio.Track).To(Equal(libregraph.PtrInt32(34)))
+				Expect(audio.TrackCount).To(Equal(libregraph.PtrInt32(99)))
+				Expect(audio.Year).To(Equal(libregraph.PtrInt32(2004)))
+			})
+		})
+
+	})
 })
diff --git a/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature b/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature
index 14fdafd4731..1e5cc80e9b8 100644
--- a/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature
+++ b/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature
@@ -241,3 +241,23 @@ Feature: copying file using file id
       | dav-path                          |
       | /remote.php/dav/spaces/<<FILEID>> |
       | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: copy a file between two project spaces
+    Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
+    And user "Alice" has created a space "first-project-space" with the default quota using the Graph API
+    And user "Alice" has created a space "second-project-space" with the default quota using the Graph API
+    And user "Alice" has uploaded a file inside space "first-project-space" with content "data from first project space" to "firstProjectSpacetextfile.txt"
+    And user "Alice" has uploaded a file inside space "second-project-space" with content "data from second project space" to "secondProjectSpacetextfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" copies a file "/secondProjectSpacetextfile.txt" into "/" inside space "first-project-space" using file-id path "<dav-path>"
+    Then the HTTP status code should be "201"
+    And for user "Alice" folder "/" of the space "first-project-space" should contain these files:
+      | firstProjectSpacetextfile.txt  |
+      | secondProjectSpacetextfile.txt |
+    And for user "Alice" folder "/" of the space "second-project-space" should contain these files:
+      | secondProjectSpacetextfile.txt |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |